PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / _front_end / class-fe-form-source-resolver.php

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

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