PluginProbe
Booking Calendar / 11.2
Booking Calendar v11.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-form-source-resolver.php

class-fe-form-source-resolver.php in Booking Calendar 11.2, at includes/_front_end/class-fe-form-source-resolver.php

596 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Front-End Booking Form Source Resolver
4 *
5 * Decides which engine to use to build the booking form body:
6 * - bfb_db : load exported shortcodes from booking_form_structures (only when BFB enabled)
7 * - legacy : use legacy wpdev_bk_personal->get_booking_form()
8 * - simple : fallback wpbc_simple_form__get_booking_form__as_html()
9 *
10 * Keeps legacy behavior by default:
11 * - If BFB is disabled => never touches DB and returns legacy/simple only.
12 *
13 * @package Booking Calendar
14 * @since 11.0.x
15 * @file ../includes/_front_end/class-fe-form-source-resolver.php
16 */
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * MultiUser ownership helper (core-safe; MU add-on hooks via filters).
24 *
25 * @since 11.0.x
26 */
27 class WPBC_FE_MU {
28
29
30 public static function get_owner_user_id_for_resource( $resource_id ) {
31
32 $resource_id = (int) $resource_id;
33
34 $user_id = apply_bk_filter( 'get_user_of_this_bk_resource', false, $resource_id );
35 if ( $user_id ) {
36 return $user_id;
37 }
38
39 return 0;
40 }
41
42
43 public static function is_user_super_booking_admin( $user_id ) {
44
45 $user_id = intval( $user_id );
46
47 if ( ( class_exists( 'wpdev_bk_multiuser' ) ) && ( $user_id > 0 ) ) {
48 $is_user_super_admin = apply_bk_filter( 'is_user_super_admin', $user_id );
49 if ( ! $is_user_super_admin ) {
50 return false;
51 }
52 }
53
54 return true;
55 }
56
57 public static function get_current_user_id() {
58 return wpbc_get_current_user_id();
59 }
60 }
61
62
63 /**
64 * Check if we enabled BFB in Settings, e.g.: WPBC_Frontend_Settings::is_bfb_enabled()
65 */
66 class WPBC_Frontend_Settings {
67
68 /**
69 * Is the BFB feature compiled/available in this build.
70 *
71 * @return bool
72 */
73 public static function is_bfb_feature_available() {
74 return ( defined( 'WPBC_NEW_FORM_BUILDER' ) && WPBC_NEW_FORM_BUILDER );
75 }
76
77 /**
78 * Is BFB enabled in settings.
79 *
80 * @param WPBC_Frontend_Context|null $ctx Optional, for future use (preview context etc).
81 *
82 * @return bool
83 */
84 public static function is_bfb_enabled( $ctx = null ) {
85
86 if ( ! self::is_bfb_feature_available() ) {
87 return false;
88 }
89
90 $is_enabled = ( 'On' === get_bk_option( 'booking_use_bfb_form' ) );
91
92 /**
93 * Allow forcing enable/disable externally if needed.
94 *
95 * @param bool $is_enabled
96 * @param WPBC_Frontend_Context|null $ctx
97 */
98 $is_enabled = (bool) apply_filters( 'wpbc_frontend_is_bfb_enabled', $is_enabled, $ctx );
99
100 return $is_enabled;
101 }
102 }
103
104
105 /**
106 * Resolver: decides BFB vs legacy vs simple and returns normalized loader args.
107 *
108 * @since 11.0.x
109 */
110 class WPBC_FE_Form_Source_Resolver {
111
112 /**
113 * Resolve form source.
114 *
115 * Input keys:
116 * - resource_id (int)
117 * - form_slug (string) // from shortcode form_type
118 * - form_status (string) // published|preview
119 * - custom_params (array) // parsed from options parser
120 * - legacy_instance (wpdev_booking|null) // optional
121 *
122 * Output:
123 * - engine: 'bfb_db'|'legacy'|'simple'
124 * - apply_after_load_filter: bool
125 * - bfb_loader_args: array
126 * - fallback_chain: array
127 *
128 * @param array $req
129 *
130 * @return array
131 */
132 public static function resolve( $req ) {
133
134 $req = is_array( $req ) ? $req : array();
135
136 $resource_id = isset( $req['resource_id'] ) ? (int) $req['resource_id'] : 0;
137 $form_slug_raw = isset( $req['form_slug'] ) ? (string) $req['form_slug'] : '';
138 $form_status_raw = isset( $req['form_status'] ) ? (string) $req['form_status'] : '';
139 $custom_params = ( isset( $req['custom_params'] ) && is_array( $req['custom_params'] ) ) ? $req['custom_params'] : array();
140
141 $legacy_instance = isset( $req['legacy_instance'] ) ? $req['legacy_instance'] : null;
142
143 // ---------------------------------------------------------------------
144 // Step A: determine slug + status (contract).
145 // ---------------------------------------------------------------------
146 $form_slug = sanitize_text_field( $form_slug_raw );
147 if ( '' === $form_slug ) {
148 $form_slug = 'standard';
149 }
150
151 $status = sanitize_key( $form_status_raw );
152
153 // Normalize synonyms.
154 if ( in_array( $status, array( 'publish', 'published' ), true ) ) {
155 $status = 'published';
156 }
157 if ( 'preview' !== $status ) {
158 $status = 'published';
159 }
160
161 // ---------------------------------------------------------------------
162 // If BFB is NOT enabled => legacy only (no DB touches).
163 // ---------------------------------------------------------------------
164 if ( ! class_exists( 'WPBC_Frontend_Settings' ) || ! WPBC_Frontend_Settings::is_bfb_enabled( null ) ) {
165 return self::fallback_to_legacy_or_simple( $legacy_instance );
166 }
167
168 // If BFB runtime is not present => legacy only.
169 if ( ! class_exists( 'WPBC_BFB_Form_Loader' ) ) {
170 return self::fallback_to_legacy_or_simple( $legacy_instance );
171 }
172
173 // If storage is not available => legacy only.
174 if ( ! class_exists( 'WPBC_BFB_Form_Storage' ) || ! method_exists( 'WPBC_BFB_Form_Storage', 'get_form_row_by_key' ) ) {
175 return self::fallback_to_legacy_or_simple( $legacy_instance );
176 }
177
178 // Optional: if table does not exist, skip DB.
179 if ( function_exists( 'wpbc_is_table_exists' ) && ! wpbc_is_table_exists( 'booking_form_structures' ) ) {
180 return self::fallback_to_legacy_or_simple( $legacy_instance );
181 }
182
183 // ---------------------------------------------------------------------
184 // Step B: determine owner_user_id (MultiUser, filterable).
185 // ---------------------------------------------------------------------
186 $owner_user_id = 0;
187
188 // Caller may explicitly pass owner_user_id for future scopes.
189 if ( isset( $req['owner_user_id'] ) ) {
190 $owner_user_id = max( 0, (int) $req['owner_user_id'] );
191 } else {
192 $owner_user_id = WPBC_FE_MU::get_owner_user_id_for_resource( $resource_id );
193 $is_super_user = WPBC_FE_MU::is_user_super_booking_admin( $owner_user_id );
194
195 // If owner_user_id is is_super_user booking admin => treat as global (0).
196 if ( $is_super_user ) {
197 $owner_user_id = 0;
198 } else {
199 $owner_user_id = max( 0, (int) $owner_user_id );
200
201 if ( $owner_user_id > 0 ) {
202 // If this is "Regular user"? Check if we can load "custom booking form" ?
203 if ( ( get_bk_option( 'booking_is_custom_forms_for_regular_users' ) !== 'On' ) && ( 'standard' !== $form_slug ) ) {
204 $form_slug = 'standard';
205 }
206 }
207 }
208 }
209
210
211 // ---------------------------------------------------------------------
212 // Step C: query order (owner_user_id -> global -> standard fallback).
213 // Resolver decides the final key to ask the loader for.
214 // ---------------------------------------------------------------------
215 $fallback_chain = array();
216
217 // Info: Hook for addons.
218 $allow_standard_fallback = (bool) apply_filters( 'wpbc_fe_resolver_allow_fallback_to_standard', true, $form_slug, $status, $owner_user_id, $req );
219
220 // Try: (slug,status,owner_user_id) then (slug,status,global) Return like this: $found = [ "form_id":1, "form_slug":"standard", "status":"published", "owner_user_id":0 ]
221 $found = self::try_find_row( $form_slug, $status, $owner_user_id, $fallback_chain );
222
223
224 // If not found, and user is not super booking admin, then find row for "SUPER BOOKING ADMIN".
225 // INFO: block this falback. 2026-03-05 20:25. Do not fallback to supr booking admin user. Fallback to legacy mode, instead.
226 // if ( ! $found && ( $owner_user_id > 0 ) ) {
227 // $found = self::try_find_row( $form_slug, $status, 0, $fallback_chain );
228 // }
229
230 // If missing and slug != standard => try STANDARD (same status).
231 if ( ! $found && $allow_standard_fallback && ( 'standard' !== $form_slug ) ) {
232
233 $found = self::try_find_row( 'standard', $status, $owner_user_id, $fallback_chain );
234
235 // The same for the super booking admin.
236 if ( ! $found && ( $owner_user_id > 0 ) ) {
237 $found = self::try_find_row( 'standard', $status, 0, $fallback_chain );
238 }
239 }
240
241 if ( $found ) {
242
243 // Build args for BFB loader.
244 $bfb_args = array(
245 'form_slug' => (string) $found['form_slug'],
246 'status' => (string) $found['status'],
247 'owner_user_id' => (int) $found['owner_user_id'],
248 'resource_id' => (int) $resource_id,
249 );
250
251 // Pass form_id if known (fast, deterministic).
252 if ( ! empty( $found['form_id'] ) ) {
253 $bfb_args['form_id'] = (int) $found['form_id'];
254 }
255
256 $result = array(
257 'engine' => 'bfb_db',
258 'apply_after_load_filter'=> true,
259 'bfb_loader_args' => $bfb_args,
260 'fallback_chain' => $fallback_chain,
261 );
262
263 /**
264 * // Info: Hook for addons. Final override point (future scopes, preview rules, etc).
265 *
266 * @param array $result
267 * @param array $req
268 */
269 return (array) apply_filters( 'wpbc_fe_form_source_resolution', $result, $req );
270 }
271
272 // Not found in DB => legacy fallback (existing behavior).
273 return self::fallback_to_legacy_or_simple( $legacy_instance );
274 }
275
276 // ---------------------------------------------------------------------
277 // Internals
278 // ---------------------------------------------------------------------
279
280 /**
281 * Try find DB row for (slug,status,owner) and record attempt in chain.
282 *
283 * @param string $slug
284 * @param string $status
285 * @param int $owner_user_id
286 * @param array $chain (by ref)
287 *
288 * @return array|false
289 */
290 private static function try_find_row( $slug, $status, $owner_user_id, &$chain ) {
291
292 $slug = sanitize_text_field( (string) $slug );
293 $status = sanitize_key( (string) $status );
294 $owner_user_id = max( 0, (int) $owner_user_id );
295
296 $chain[] = array(
297 'form_slug' => $slug,
298 'status' => $status,
299 'owner_user_id' => $owner_user_id,
300 );
301 // Here we already have the booking form configuration: row.advanced_form = "<div>..." also row.content_form = '...', row.settings_json = '{...}'
302 $row = WPBC_BFB_Form_Storage::get_form_row_by_key( $slug, $status, $owner_user_id );
303
304 if ( ! $row || empty( $row->booking_form_id ) ) {
305 return false;
306 }
307
308 return array(
309 'form_id' => (int) $row->booking_form_id,
310 'form_slug' => $slug,
311 'status' => $status,
312 'owner_user_id' => isset( $row->owner_user_id ) ? max( 0, (int) $row->owner_user_id ) : $owner_user_id,
313 );
314 }
315
316 /**
317 * Legacy/simple fallback result.
318 *
319 * @param mixed $legacy_instance
320 *
321 * @return array
322 */
323 private static function fallback_to_legacy_or_simple( $legacy_instance ) {
324
325 if ( ( ! empty( $legacy_instance ) ) && ( false !== $legacy_instance->wpdev_bk_personal ) && ( 'On' != get_bk_option( 'booking_is_use_simple_booking_form' ) ) ) {
326 return array(
327 'engine' => 'legacy',
328 'apply_after_load_filter' => false,
329 'bfb_loader_args' => array(),
330 'fallback_chain' => array(),
331 );
332 }
333
334 return array(
335 'engine' => 'simple',
336 'apply_after_load_filter' => true,
337 'bfb_loader_args' => array(),
338 'fallback_chain' => array(),
339 );
340 }
341 }
342
343
344 /**
345 * BFB resolver for "Booking Form Data" template (content_form).
346 *
347 * Purpose:
348 * - When BFB DB engine is used for a form, the correct booking "data template"
349 * must come from wp_booking_form_structures.content_form (not booking_form_show option).
350 * - This is used by wpbc_get__booking_form_data__show() to replace [field] shortcodes
351 * with values from booking form_data string.
352 *
353 * Design notes:
354 * - Uses WPBC_FE_Form_Source_Resolver::resolve() to keep the same decision chain as front-end rendering.
355 * - Reads the template via wpbc_bfb_get_booking_form_pair() (must expose content_form).
356 * - Safe no-op when BFB / resolver is not available.
357 *
358 * @since 11.0.x
359 */
360 class WPBC_BFB_Booking_Data_Content_Resolver {
361
362 /**
363 * Try to override legacy $booking_form_show by BFB content_form (resolver-driven).
364 *
365 * @param string $booking_form_show Legacy resolved booking_form_show.
366 * @param int $resource_id Booking resource ID.
367 * @param string $form_slug Form slug/name (legacy: 'standard' or custom form name).
368 * @param string $form_data Booking form_data string from DB.
369 * @param array $params Optional. Context override (e.g. form_status/preview token).
370 *
371 * @return string Possibly overridden template.
372 */
373 public static function maybe_override_booking_form_show_by_bfb( $booking_form_show, $resource_id, $form_slug, $form_data, $params = array() ) {
374
375 $booking_form_show = (string) $booking_form_show;
376 $resource_id = (int) $resource_id;
377 $form_slug = (string) $form_slug;
378 $form_data = (string) $form_data;
379
380 if ( '' === $form_slug ) {
381 $form_slug = 'standard';
382 }
383
384 // Hard requirements for BFB resolver path.
385 if ( ! class_exists( 'WPBC_FE_Form_Source_Resolver' ) ) {
386 return $booking_form_show;
387 }
388 if ( ! class_exists( 'WPBC_BFB_Form_Loader' ) ) {
389 return $booking_form_show;
390 }
391
392 // ------------------------------------------------------------
393 // Preview-aware context (critical fix).
394 // ------------------------------------------------------------
395 $ctx = array();
396 if ( function_exists( 'wpbc_get_request_form_context' ) ) {
397 $ctx = wpbc_get_request_form_context();
398 }
399 $ctx = wp_parse_args( ( is_array( $params ) ? $params : array() ), ( is_array( $ctx ) ? $ctx : array() ) );
400
401 $form_status = ( isset( $ctx['form_status'] ) ) ? sanitize_key( $ctx['form_status'] ) : 'published';
402 if ( in_array( $form_status, array( 'publish', 'published' ), true ) ) {
403 $form_status = 'published';
404 }
405 if ( 'preview' !== $form_status ) {
406 $form_status = 'published';
407 }
408
409 // If preview session has exported content_form payload => use it (fast + correct).
410 if ( ( 'preview' === $form_status ) && function_exists( 'wpbc_bfb_preview__maybe_get_payload_from_context' ) ) {
411
412 $payload = wpbc_bfb_preview__maybe_get_payload_from_context( $ctx );
413
414 if ( ! empty( $payload ) && isset( $payload['content_form'] ) && ( '' !== (string) $payload['content_form'] ) ) {
415
416 $content_form = (string) $payload['content_form'];
417
418 // Keep legacy behavior: custom html shortcodes + language.
419 $content_form = wpbc_bf__replace_custom_html_shortcodes( $content_form );
420 $content_form = wpbc_lang( $content_form );
421
422 return $content_form;
423 }
424 }
425
426 // ------------------------------------------------------------
427 // Resolver-driven DB load (status must NOT be hard-coded).
428 // ------------------------------------------------------------
429 $custom_params = array();
430
431 // Optional: allow future-proof explicit form_id embedded into booking form_data.
432 $bfb_form_id = self::get_bfb_form_id_from_form_data( $form_data, $resource_id );
433 if ( $bfb_form_id > 0 ) {
434 $custom_params['bfb_form_id'] = (int) $bfb_form_id;
435 }
436
437 $req = array(
438 'resource_id' => $resource_id,
439 'form_slug' => $form_slug,
440 'form_status' => $form_status, // <-- critical: use preview/published
441 'custom_params' => $custom_params,
442 'legacy_instance' => null,
443
444 // Future-proof: let filters access the full preview context if they need it.
445 'ctx' => $ctx,
446 );
447
448 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
449
450 // If preview status is requested but nothing found, try published as fallback (keeps BFB).
451 if ( ( empty( $resolved['engine'] ) || ( 'bfb_db' !== $resolved['engine'] ) ) && ( 'preview' === $form_status ) ) {
452 $req['form_status'] = 'published';
453 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
454 }
455
456 if ( empty( $resolved['engine'] ) || ( 'bfb_db' !== $resolved['engine'] ) ) {
457 return $booking_form_show;
458 }
459
460 $bfb_loader_args = array();
461 if ( ! empty( $resolved['bfb_loader_args'] ) && is_array( $resolved['bfb_loader_args'] ) ) {
462 $bfb_loader_args = $resolved['bfb_loader_args'];
463 }
464
465 // Keep old behavior: allow explicit override from booking data (if present).
466 if ( ( $bfb_form_id > 0 ) && empty( $bfb_loader_args['form_id'] ) ) {
467 $bfb_loader_args['form_id'] = (int) $bfb_form_id;
468 }
469
470 // Signal loader that we need the "content_form" (booking fields data template).
471 $bfb_loader_args['return'] = 'content_form';
472
473 $bfb_pair = wpbc_bfb_get_booking_form_pair( $bfb_loader_args );
474
475 $content_form = self::extract_content_form_from_pair( $bfb_pair );
476
477 if ( '' === trim( $content_form ) ) {
478 return $booking_form_show;
479 }
480
481 // Keep legacy behavior: custom html shortcodes + language.
482 $content_form = wpbc_bf__replace_custom_html_shortcodes( $content_form );
483 $content_form = wpbc_lang( $content_form );
484
485 return $content_form;
486 }
487
488
489 /**
490 * Extract content template from loader return (supports multiple formats).
491 *
492 * Supported formats:
493 * 1) array( 'content' => '...' ) <- current WPBC_BFB_Form_Loader output
494 * 2) array( 'content_form' => '...' ) <- optional future alias
495 * 3) array( 'form' => array( 'content' => '...' ) ) <- future-proof
496 * 4) array( 'form' => array( 'content_form' => '...' ) ) <- future-proof
497 *
498 * @param mixed $bfb_pair Loader result.
499 *
500 * @return string
501 */
502 private static function extract_content_form_from_pair( $bfb_pair ) {
503
504 if ( ! is_array( $bfb_pair ) ) {
505 return '';
506 }
507
508 /**
509 * Safety: if loader fell back to legacy, do NOT override booking_form_show.
510 * WPBC_BFB_Form_Loader returns 'source' => 'builder'|'legacy'.
511 */
512 if ( isset( $bfb_pair['source'] ) && ( 'builder' !== $bfb_pair['source'] ) ) {
513 return '';
514 }
515
516 // Current loader output key.
517 if ( isset( $bfb_pair['content'] ) && is_string( $bfb_pair['content'] ) ) {
518 return (string) $bfb_pair['content'];
519 }
520
521 // Optional alias (if some implementations already return it).
522 if ( isset( $bfb_pair['content_form'] ) && is_string( $bfb_pair['content_form'] ) ) {
523 return (string) $bfb_pair['content_form'];
524 }
525
526 // Future-proof nested formats.
527 if ( isset( $bfb_pair['form'] ) && is_array( $bfb_pair['form'] ) ) {
528
529 if ( isset( $bfb_pair['form']['content'] ) && is_string( $bfb_pair['form']['content'] ) ) {
530 return (string) $bfb_pair['form']['content'];
531 }
532
533 if ( isset( $bfb_pair['form']['content_form'] ) && is_string( $bfb_pair['form']['content_form'] ) ) {
534 return (string) $bfb_pair['form']['content_form'];
535 }
536 }
537
538 return '';
539 }
540
541
542 /**
543 * (Optional / future-proof) Parse BFB form_id stored inside booking form_data.
544 *
545 * Supported patterns:
546 * - "wpbc_bfb_form_id{resource_id}^{ID}"
547 * - "wpbc_bfb_form_id^{ID}"
548 *
549 * @param string $form_data
550 * @param int $resource_id
551 *
552 * @return int
553 */
554 private static function get_bfb_form_id_from_form_data( $form_data, $resource_id ) {
555
556 $form_data = (string) $form_data;
557 $resource_id = (int) $resource_id;
558
559 if ( '' === $form_data ) {
560 return 0;
561 }
562
563 $prefixes = array(
564 'wpbc_bfb_form_id' . $resource_id . '^',
565 'wpbc_bfb_form_id^',
566 );
567
568 foreach ( $prefixes as $prefix ) {
569
570 $pos = strpos( $form_data, $prefix );
571 if ( false === $pos ) {
572 continue;
573 }
574
575 $chunk = substr( $form_data, $pos + strlen( $prefix ) );
576
577 // Ends at "~" if present.
578 if ( false !== strpos( $chunk, '~' ) ) {
579 $chunk = substr( $chunk, 0, strpos( $chunk, '~' ) );
580 }
581
582 $chunk = trim( $chunk );
583 if ( '' === $chunk ) {
584 continue;
585 }
586
587 $form_id = (int) $chunk;
588 if ( $form_id > 0 ) {
589 return $form_id;
590 }
591 }
592
593 return 0;
594 }
595 }
596