PluginProbe
Booking Calendar / 10.15.6
Booking Calendar v10.15.6
11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 All 204 releases
booking / includes / _front_end / class-fe-render-form-body.php

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

971 lines 36.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Front-End Form Body Rendering
4 *
5 * - Calendar markup (or hidden selected-dates field)
6 * - Form body source resolution (BFB / legacy / simple)
7 * - Legacy post-processing hooks (additional calendars, captcha, change-over times)
8 * - Legacy wrapper (<form> container + nonce + hidden fields)
9 * - Legacy inline scripts (duplicate-calendar warning, cost hints, autofill)
10 *
11 * @package Booking Calendar
12 * @since 11.0.x
13 * @file ../includes/_front_end/class-fe-render-form-body.php
14 */
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Booking Form Body Renderer.
22 *
23 * Orchestrates building of the booking form HTML while preserving legacy output and
24 * filter/action execution order.
25 *
26 * @since 11.0.x
27 */
28 class WPBC_FE_Form_Body_Renderer {
29
30 /**
31 * Render booking form HTML (calendar + form body + wrapper + inline scripts),
32 * preserving legacy output and hook execution order.
33 *
34 * Expected flow:
35 * 1) Parse shortcode "options" into custom parameters (for BFB/simple form context).
36 * 2) Build calendar markup OR hidden textarea with preselected dates.
37 * 3) Resolve form body source:
38 * - BFB source (if available) with filter override and shortcode-engine fallback.
39 * - Legacy booking form (personal.php).
40 * - Simple form fallback.
41 * 4) Post-process composed markup:
42 * - Insert calendar ([calendar] placeholder or prepend).
43 * - Replace additional calendars via legacy filter.
44 * - Replace [captcha] placeholder.
45 * - Append change-over times.
46 * 5) Wrap in legacy container + add nonce + hidden fields.
47 * 6) Append legacy inline scripts.
48 *
49 * @since 11.0.x
50 *
51 * @param array $args {
52 * Optional. Arguments for rendering.
53 *
54 * @type int $resource_id Booking resource ID.
55 * @type string $custom_booking_form Booking form slug/name (legacy: "standard").
56 * @type string $selected_dates_without_calendar Preselected dates string (legacy format).
57 * @type int $cal_count Number of months to show in calendar.
58 * @type mixed $shortcode_param__options Shortcode options string (legacy: options="...") or array.
59 * @type wpdev_booking $legacy_instance Legacy plugin booking object (Phase #1/compat).
60 * }
61 *
62 * @return string Rendered HTML (ready to echo).
63 */
64 public static function render( $args ) {
65
66 $defaults = array(
67 'resource_id' => 1,
68 'custom_booking_form' => 'standard',
69 'form_status' => 'published',
70 'selected_dates_without_calendar' => '',
71 'cal_count' => 1,
72 'shortcode_param__options' => '',
73 'legacy_instance' => null,
74 );
75
76 $args = wp_parse_args( $args, $defaults );
77
78 $resource_id = (int) $args['resource_id'];
79 $custom_booking_form = (string) $args['custom_booking_form'];
80 $selected_dates_without_calendar = (string) $args['selected_dates_without_calendar'];
81 $cal_count = (int) $args['cal_count'];
82 $shortcode_param__options = $args['shortcode_param__options'];
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 );
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, choosing BFB source if available, else legacy.
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 *
349 * @return array
350 */
351 public static function get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance ) {
352
353 $resource_id = (int) $resource_id;
354 $custom_booking_form = (string) $custom_booking_form;
355 $form_status = (string) $form_status;
356 $custom_params = ( is_array( $custom_params ) ) ? $custom_params : array();
357
358 $req = array(
359 'resource_id' => $resource_id,
360 'form_slug' => $custom_booking_form,
361 'form_status' => $form_status,
362 'custom_params' => $custom_params,
363 'legacy_instance' => $legacy_instance,
364 );
365
366 // Fix: 2026-02-18 16:51.
367 $req['current_resource_id'] = $resource_id;
368 $req['current_edit_booking'] = false;
369 $req['current_edit_booking_id'] = false;
370 // =============================================================================================================
371 // == If Booking Edit ? ==
372 // =============================================================================================================
373 // Maybe get the "custom booking form" and child "booking resource ID", if the booking edited. And this booking was created in custom booking form.
374 $maybe_edited__booking_id__resource_id = self::maybe_check_if_booking_edit();
375
376 if ( ! empty( $maybe_edited__booking_id__resource_id ) ) {
377
378 // == Edit Booking =========================================================================================
379 $req['resource_id'] = $maybe_edited__booking_id__resource_id['resource_id'];
380 $req['current_resource_id'] = $maybe_edited__booking_id__resource_id['resource_id'];
381 $req['current_edit_booking_id'] = $maybe_edited__booking_id__resource_id['booking_id'];
382
383 // Parsed booking form values.
384 $req['current_edit_booking'] = self::get_booking_data( $maybe_edited__booking_id__resource_id['booking_id'] );
385
386 // -- Is use custom form ? ---------------------------------------------------------------------------------
387
388 // Get 'custom booking form name' from URL.
389 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
390 $my_booking_form = ( ( isset( $_GET['booking_form'] ) ) ? sanitize_text_field( wp_unslash( $_GET['booking_form'] ) ) : '' );
391
392 // Otherwise get 'custom booking form name' from EDIT booking details.
393 if ( ( empty( $my_booking_form ) ) && ( ! empty( $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ] ) ) ) {
394 $my_booking_form = $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ]['value'];
395 }
396
397 // If not '' then OVVERIDE.
398 if ( ! empty( $my_booking_form ) ) {
399 $req['form_slug'] = $my_booking_form;
400 }
401
402
403 // -- Check out dates ? ------------------------------------------------------------------------------------
404 if (
405 ( 'On' === get_bk_option( 'booking_last_checkout_day_available' ) ) &&
406 ( ! empty( $req['current_edit_booking']['dates'] ) )
407 ) {
408 // Add one additional date for editing, if "last_checkout_day_available" is "On".
409 // Dates in format: [ ... 2: 2019-03-13 00:00:00, 3: 2019-03-14 12:00:02 ].
410 $last_day = $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ];
411 $check_out = strtotime( $last_day );
412 $check_out = strtotime( '+1 day', $check_out );
413 $last_day = date_i18n( 'Y-m-d 00:00:00', strtotime( $last_day ) );
414
415 $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ] = $last_day;
416
417 $req['current_edit_booking']['dates'][] = date_i18n( 'Y-m-d H:i:s', $check_out );
418 }
419
420 // -- Check out dates ? ------------------------------------------------------------------------------------
421 // Check when we edit "child resource" -> need re-update ID in calendar and form elements to have parent resource // FixIn: 6.1.1.9.
422 // FixIn: 10.10.1.2 ( ! isset( $_GET['resource_no_update'] ) ) // FixIn: 9.4.2.3.
423 if (
424 ( function_exists( 'wpbc_is_this_child_resource' ) ) &&
425 ( wpbc_is_this_child_resource( $req['resource_id'] ) )
426 ) {
427 $bk_parent_br_id = wpbc_get_parent_resource( $req['resource_id'] );
428
429 $new_booking_data_arr = array();
430
431 foreach ( $req['current_edit_booking']['parsed_form'] as $my_key_old => $booking_data ) {
432
433 $new_booking_data_arr[ $booking_data['element_name'] . $bk_parent_br_id ] = $booking_data;
434 }
435
436 $req['current_edit_booking']['parsed_form'] = $new_booking_data_arr;
437 $req['resource_id'] = $bk_parent_br_id;
438 $req['current_resource_id'] = $bk_parent_br_id;
439 }
440 }
441
442 // Update after “edit booking” resource overrides.
443 $resource_id = (int) $req['resource_id'];
444 $custom_booking_form = (string) $req['form_slug'];
445
446 /**
447 * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple'
448 * 'apply_after_load_filter' : bool
449 * 'bfb_loader_args' : array()
450 * 'fallback_chain' : array()
451 */
452 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
453
454 // -----------------------------------------------------------------
455 // BFB DB engine (only if resolver decided it).
456 // -----------------------------------------------------------------
457 if ( ( isset( $resolved['engine'] ) ) && ( 'bfb_db' === $resolved['engine'] ) && function_exists( 'wpbc_bfb_get_booking_form_pair' ) ) {
458
459 $booking_form_html__arr = self::wpbc_bfb__get_booking_form_html__arr( $req, $resolved );
460
461 if ( null !== $booking_form_html__arr ) {
462 return $booking_form_html__arr;
463 }
464 }
465
466 // -----------------------------------------------------------------
467 // Legacy engine (unchanged output).
468 // -----------------------------------------------------------------
469 if ( ( ! empty( $legacy_instance ) )
470 && ( false !== $legacy_instance->wpdev_bk_personal )
471 // && ( 'On' != get_bk_option( 'booking_is_use_simple_booking_form' ) ) // Important! in paid versions, if edited in "Simple mode" the compiled form, still saved as "Advanced form" content, so we need to get it from there!
472 ) {
473 return array(
474 'body_html' => $legacy_instance->wpdev_bk_personal->get_booking_form( $resource_id, $custom_booking_form, $custom_params ),
475 'apply_after_load_filter' => false,
476 'bfb_settings' => array(),
477 );
478 }
479
480 // -----------------------------------------------------------------
481 // Simple fallback. - ONLY for the Booking Calendar Free version !
482 // -----------------------------------------------------------------
483 return array(
484 'body_html' => wpbc_simple_form__get_booking_form__as_html( $resource_id, $custom_booking_form, $custom_params ),
485 'apply_after_load_filter' => true,
486 'bfb_settings' => array(),
487 );
488 }
489
490
491 /**
492 * Get BFB booking form content array. Helper function.
493 *
494 * @param array $req - array.
495 * @param array $resolved - array.
496 *
497 * @return array|null
498 */
499 public static function wpbc_bfb__get_booking_form_html__arr( $req, $resolved ) {
500
501 $custom_params = $req ['custom_params'];
502 $form_status = $req ['form_status'];
503 $custom_booking_form = $req['form_slug'];
504 $resource_id = $req ['resource_id'];
505 $legacy_instance = $req ['legacy_instance'];
506
507 $bfb_loader_args = ( isset( $resolved['bfb_loader_args'] ) && is_array( $resolved['bfb_loader_args'] ) ) ? $resolved['bfb_loader_args'] : array();
508
509
510 // Keep old behavior: allow explicit form_id/status overrides from options only if present.
511 if ( ! empty( $custom_params['bfb_form_id'] ) && empty( $bfb_loader_args['form_id'] ) ) {
512 $bfb_loader_args['form_id'] = (int) $custom_params['bfb_form_id'];
513 }
514
515 $bfb_settings = array();
516 $bfb_source = trim( '' );
517 $settings_json = trim( '' );
518
519 $bfb_pair = wpbc_bfb_get_booking_form_pair( $bfb_loader_args );
520
521 if ( is_array( $bfb_pair ) ) {
522 $bfb_source = isset( $bfb_pair['form'] ) ? (string) $bfb_pair['form'] : '';
523 $settings_json = isset( $bfb_pair['settings_json'] ) ? (string) $bfb_pair['settings_json'] : '';
524 }
525
526 if ( '' !== $settings_json ) {
527 $decoded = json_decode( $settings_json, true );
528 if ( is_array( $decoded ) ) {
529 $bfb_settings = $decoded;
530 }
531 }
532
533 // Allow loader to return either string OR array with settings.
534 if ( is_array( $bfb_source ) ) {
535
536 // Common possible keys (support multiple formats, future-proof).
537 if ( ! empty( $bfb_source['settings'] ) && is_array( $bfb_source['settings'] ) ) {
538 $bfb_settings = $bfb_source['settings'];
539 } elseif ( ! empty( $bfb_source['settings_json'] ) && is_string( $bfb_source['settings_json'] ) ) {
540 $decoded = json_decode( $bfb_source['settings_json'], true );
541 if ( is_array( $decoded ) ) {
542 $bfb_settings = $decoded;
543 }
544 }
545
546 // Source itself.
547 if ( isset( $bfb_source['advanced_form'] ) ) {
548 $bfb_source = (string) $bfb_source['advanced_form'];
549 } elseif ( isset( $bfb_source['source'] ) ) {
550 $bfb_source = (string) $bfb_source['source'];
551 } else {
552 $bfb_source = '';
553 }
554 }
555
556 // Optional fallback hook if you keep loader returning string for now.
557 if ( empty( $bfb_settings ) ) {
558 $bfb_settings = apply_filters( 'wpbc_bfb_form_settings_for_render', array(), $bfb_loader_args, $resource_id, $custom_booking_form, $custom_params );
559 }
560
561
562 if ( '' !== trim( (string) $bfb_source ) ) {
563
564 // Info: Hook for addons. Give addons a chance to fully handle rendering.
565 $bfb_form_html = apply_filters( 'wpbc_bfb_render_booking_form_source', '', $bfb_source, $resource_id, $custom_booking_form, $custom_params );
566
567 if ( '' === $bfb_form_html ) {
568
569 $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) );
570
571 $bfb_source = wpbc_lang( $bfb_source );
572
573 // Replace custom params in the source (legacy behavior).
574 if ( ! empty( $custom_params ) ) {
575 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
576 $bfb_source = str_replace( $custom_params_key, $custom_params_value, $bfb_source );
577 }
578 }
579
580 $bfb_source = wpbc_bf__replace_custom_html_shortcodes( $bfb_source );
581
582 $bfb_form_html = wpbc_render_booking_form_shortcodes( $bfb_source, $render_args, $req );
583 }
584
585 if ( '' !== $bfb_form_html ) {
586 return array(
587 'body_html' => $bfb_form_html,
588 'apply_after_load_filter' => ! empty( $resolved['apply_after_load_filter'] ),
589 'bfb_settings' => $bfb_settings,
590 );
591 }
592 }
593
594 // If BFB resolution succeeded but returned empty output, fall through to legacy.
595 return null;
596 }
597 }
598
599 /**
600 * Booking form post-processor (legacy compatibility).
601 *
602 * Applies legacy transformations and hooks to the combined form markup:
603 * - Inserts calendar markup (replaces [calendar] or prepends).
604 * - Replaces additional calendars via legacy filter.
605 * - Replaces [captcha] placeholder if present.
606 * - Appends change-over times.
607 *
608 * @since 11.0.x
609 */
610 class WPBC_FE_Form_Postprocessor {
611
612 /**
613 * Apply legacy postprocessing to the composed form markup.
614 *
615 * @since 11.0.x
616 *
617 * @param string $form_html Booking form body HTML.
618 * @param string $calendar_html Calendar HTML snippet.
619 * @param array $ctx {
620 * Context data used during post-processing.
621 *
622 * @type int $resource_id Booking resource ID.
623 * @type string $custom_booking_form Booking form slug/name.
624 * @type string $selected_dates_without_calendar Preselected dates (legacy string).
625 * @type int $cal_count Number of months shown.
626 * @type mixed $shortcode_param__options Shortcode options.
627 * @type wpdev_booking $legacy_instance Legacy booking instance (optional).
628 * @type string $nl Legacy separator markup.
629 * }
630 *
631 * @return string Post-processed HTML.
632 */
633 public static function apply( $form_html, $calendar_html, $ctx ) {
634
635 $form_html = (string) $form_html;
636 $calendar_html = (string) $calendar_html;
637
638 $resource_id = (int) $ctx['resource_id'];
639 $custom_booking_form = (string) $ctx['custom_booking_form'];
640 $selected_dates_without_calendar = (string) $ctx['selected_dates_without_calendar'];
641 $cal_count = (int) $ctx['cal_count'];
642 $shortcode_param__options = $ctx['shortcode_param__options'];
643 $legacy_instance = $ctx['legacy_instance'];
644 $nl = (string) $ctx['nl'];
645
646 // Insert calendar into form.
647 if ( false !== strpos( $form_html, '[calendar]' ) ) {
648 $form_html = str_replace( '[calendar]', $calendar_html, $form_html );
649 } else {
650 // FixIn: 2981-01-13 13 Jan 2981.
651 //$form_html = '<div class="booking_form_div">' . $calendar_html . '</div>' . $nl . $form_html;
652 $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;
653 }
654
655 // Replace additional calendars.
656 $form_html = apply_bk_filter(
657 'wpdev_check_for_additional_calendars_in_form',
658 $form_html,
659 $resource_id,
660 array(
661 'booking_form' => $custom_booking_form,
662 'selected_dates' => $selected_dates_without_calendar,
663 'cal_count' => $cal_count,
664 'options' => $shortcode_param__options,
665 )
666 );
667
668 // Captcha replacement.
669 $form_html = apply_filters( 'wpbc_booking_form_html__create_captcha', $form_html, $resource_id );
670
671 // Change-over times injection.
672 $form_html = apply_filters( 'wpbc_booking_form_html__update__append_change_over_times', $form_html, $resource_id );
673
674 return $form_html;
675 }
676 }
677
678 /**
679 * Booking form wrapper (legacy HTML structure).
680 *
681 * Responsible for building the outer <div> container and <form> tag, including:
682 * - ajax response containers
683 * - anchor link (bklnk)
684 * - hidden bk_type field
685 * - nonce field
686 * - "garbage" container used by legacy JS
687 *
688 * @since 11.0.x
689 */
690 class WPBC_FE_Form_Wrapper {
691
692 /**
693 * Wrap the form HTML into legacy outer container + add hidden fields + nonce + garbage.
694 *
695 * @param string $form_html
696 * @param int $resource_id
697 *
698 * @param string $form_html Post-processed booking form HTML (already contains calendar markup).
699 * @param int $resource_id Booking resource ID.
700 *
701 * @return string Wrapped HTML.
702 */
703 public static function wrap( $form_html, $resource_id ) {
704
705 $resource_id = (int) $resource_id;
706 $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
707
708 $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
709 $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
710 $booking_form_theme = get_bk_option( 'booking_form_theme' );
711
712 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
713 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
714 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
715
716 $return_form = '<div id="' . esc_attr( $form_container_random_id ) . '" class="' . esc_attr( $form_container_css ) . '" >' .
717 '<form id="booking_form' . intval( $resource_id ) . '" class="booking_form ' . esc_attr( $booking_form_format_type ) . '" method="post" action="">' .
718 '<div id="ajax_respond_insert' . intval( $resource_id ) . '" class="ajax_respond_insert" style="display:none;"></div>' .
719 '<a name="bklnk' . $resource_id . '" id="bklnk' . $resource_id . '"></a>' .
720 '<div id="booking_form_div' . $resource_id . '" class="booking_form_div">' .
721 $form_html .
722 '<input id="bk_type' . $resource_id . '" name="bk_type' . $resource_id . '" class="" type="hidden" value="' . $resource_id . '" />' .
723 '</div>' .
724 '<div id="submiting' . $resource_id . '"></div>' .
725 '<div class="form_bk_messages" id="form_bk_messages' . $resource_id . '" ></div>' .
726 $wpbc_nonce .
727 '</form>' .
728 '</div>' .
729 '<div id="booking_form_garbage' . intval( $resource_id ) . '" class="booking_form_garbage"></div>';
730
731 return $return_form;
732 }
733 }
734
735 // ---------------------------------------------------------------------------------------------------------------------
736
737 /**
738 * Inline scripts builder (legacy compatibility).
739 *
740 * Generates and appends legacy inline JavaScript required by booking form UX:
741 * - Duplicate-calendar warning in console when the same resource calendar is rendered multiple times.
742 * - Cost hint display trigger when selected dates are predefined.
743 * - Autofill for logged-in users (legacy behavior).
744 *
745 * @since 11.0.x
746 */
747 class WPBC_FE_Inline_Scripts {
748
749 /**
750 * Collect legacy inline scripts into WPBC_FE_Assets (Elementor-safe).
751 *
752 * @param int $resource_id - ID of booking resource.
753 *
754 * @return string Inline scripts HTML.
755 */
756 public static function collect( $resource_id ) {
757
758 // FixIn: 10.14.17.2.
759
760 $html = '';
761 $resource_id = (int) $resource_id;
762
763 // == Autofill (legacy conditions preserved) ==
764 $autofill_js = self::build_autofill_js_body( $resource_id );
765 if ( '' !== $autofill_js ) {
766 $assets_key = 'wpbc:autofill:' . intval( $resource_id );
767 $added = WPBC_FE_Assets::add_jq_ready_js_to_wp_script( 'wpbc_all', $autofill_js, $assets_key );
768 if ( ! $added ) {
769 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key ); // Fallback to direct inline JS, if previosly not edded script.
770 }
771 }
772
773 return $html;
774 }
775
776 /**
777 * Build ONLY the JS BODY for legacy autofill (no <script>, no ready wrapper).
778 *
779 * Now calls the shared JS function:
780 * window.WPBC_FE.autofill_booking_form_fields( resource_id, fill_values )
781 *
782 * @param int $resource_id Booking resource ID.
783 *
784 * @return string JS body (or empty string if not applicable).
785 */
786 private static function build_autofill_js_body( $resource_id ) {
787
788 $resource_id = (int) $resource_id;
789
790 if ( WPBC_GET_Request::has_non_empty_get( 'booking_hash' ) ) {
791 return '';
792 }
793
794 $is_use_auto_fill_for_logged = get_bk_option( 'booking_is_use_autofill_4_logged_user' );
795 if ( 'On' !== $is_use_auto_fill_for_logged ) {
796 return '';
797 }
798
799 $curr_user = wpbc_get_current_user();
800 if ( empty( $curr_user ) || ( (int) $curr_user->ID <= 0 ) ) {
801 return '';
802 }
803
804 $user_nick_name = get_user_meta( $curr_user->ID, 'nickname' );
805 $user_nick_name = ( empty( $user_nick_name ) ) ? '' : $user_nick_name[0];
806
807 $fill_values = array(
808 'nickname' => (string) $user_nick_name,
809 'last_name' => (string) $curr_user->last_name,
810 'first_name' => (string) $curr_user->first_name,
811 'email' => (string) $curr_user->user_email,
812 'phone' => (string) $curr_user->phone_number,
813 'nb_enfant' => (string) $curr_user->nb_enfant,
814 'url' => (string) $curr_user->user_url,
815 );
816
817 // IMPORTANT: return body only (no <script>, no ready wrapper).
818 $js_body = 'if ( window.WPBC_FE && ( typeof window.WPBC_FE.autofill_booking_form_fields === "function" ) ) {';
819 $js_body .= 'window.WPBC_FE.autofill_booking_form_fields(' . (int) $resource_id . ', ' . wp_json_encode( $fill_values ) . ');';
820 $js_body .= '}';
821
822 return $js_body;
823 }
824 }
825
826 class WPBC_FE_Form_Style_Injector {
827
828 /**
829 * Inject CSS variables into the FIRST <div ... class="... wpbc_bfb_form ..."> tag.
830 *
831 * @param string $html
832 * @param array $css_vars Map: '--var-name' => 'value'
833 *
834 * @return string
835 */
836 public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
837
838 $html = (string) $html;
839 $css_vars = is_array( $css_vars ) ? $css_vars : array();
840
841 if ( empty( $css_vars ) ) {
842 return $html;
843 }
844 if ( false === strpos( $html, 'wpbc_bfb_form' ) ) {
845 return $html;
846 }
847
848 $style_append = self::build_css_vars_style_fragment( $css_vars );
849 if ( '' === $style_append ) {
850 return $html;
851 }
852
853 // Match FIRST <div ... class="... wpbc_bfb_form ..."> tag.
854 $pattern = '/<div\b[^>]*\bclass\s*=\s*(["\'])(?:(?!\1).)*\bwpbc_bfb_form\b(?:(?!\1).)*\1[^>]*>/i';
855
856 $result = preg_replace_callback(
857 $pattern,
858 function( $m ) use ( $style_append ) {
859
860 $tag = $m[0];
861
862 // If style already exists: append.
863 if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
864
865 $quote = $sm[1];
866 $existing = (string) $sm[2];
867
868 // Avoid double-escaping if the tag already contains entities.
869 $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
870
871 $merged = trim( $existing );
872 if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
873 $merged .= ';';
874 }
875 $merged .= $style_append;
876
877 $tag = preg_replace(
878 '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
879 'style=' . $quote . esc_attr( $merged ) . $quote,
880 $tag,
881 1
882 );
883
884 return $tag;
885 }
886
887 // No style attr: add it.
888 $tag = rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
889
890 return $tag;
891 },
892 $html,
893 1
894 );
895
896 // preg_replace_callback can return null on regex error.
897 return ( null === $result ) ? $html : $result;
898 }
899
900 /**
901 * Build CSS vars fragment: "--a:1;--b:2;"
902 *
903 * @param array $css_vars
904 *
905 * @return string
906 */
907 private static function build_css_vars_style_fragment( $css_vars ) {
908
909 $out = '';
910
911 foreach ( $css_vars as $name => $value ) {
912
913 $name = self::sanitize_css_var_name( $name );
914 $value = self::sanitize_css_var_value( $value );
915
916 // Allow "0" but skip empty.
917 if ( '' === $name || '' === $value ) {
918 continue;
919 }
920
921 $out .= $name . ':' . $value . ';';
922 }
923
924 return $out;
925 }
926
927 private static function sanitize_css_var_name( $name ) {
928
929 $name = is_scalar( $name ) ? trim( (string) $name ) : '';
930 if ( '' === $name ) {
931 return '';
932 }
933
934 // Keep it strict: only CSS custom properties.
935 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
936 return '';
937 }
938
939 // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
940 $lower = strtolower( $name );
941 if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) ) {
942 return '';
943 }
944
945 return $name;
946 }
947
948 private static function sanitize_css_var_value( $value ) {
949
950 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
951 if ( '' === $value ) {
952 return '';
953 }
954
955 /**
956 * IMPORTANT:
957 * Disallow characters that can break out of "--var:value;" into extra declarations:
958 * - ';' would start a new declaration
959 * - '{' '}' can start blocks
960 * Also strip quotes/newlines/< > to keep inline style safe.
961 */
962 $value = str_replace(
963 array( ';', '{', '}', '"', "'", '<', '>', "\n", "\r", "\0" ),
964 '',
965 $value
966 );
967
968 return trim( $value );
969 }
970 }
971