PluginProbe
Booking Calendar / 11.8.2
Booking Calendar v11.8.2
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 10.11.3 All 202 releases
booking / includes / _front_end / class-fe-form-source-resolver.php

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

529 lines 17.1 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::missing_bfb_result( 'bfb_loader_missing' );
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::missing_bfb_result( 'bfb_storage_missing' );
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::missing_bfb_result( 'bfb_table_missing' );
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 /**
208 * // Info: Hook for addons. Final override point (future scopes, preview rules, etc).
209 *
210 * @param array $result
211 * @param array $req
212 */
213 return (array) apply_filters( 'wpbc_fe_form_source_resolution', $result, $req );
214 }
215
216 return self::missing_bfb_result( 'bfb_form_not_found', $fallback_chain );
217 }
218
219 // ---------------------------------------------------------------------
220 // Internals
221 // ---------------------------------------------------------------------
222
223 /**
224 * Try find DB row for (slug,status,owner) and record attempt in chain.
225 *
226 * @param string $slug
227 * @param string $status
228 * @param int $owner_user_id
229 * @param array $chain (by ref)
230 *
231 * @return array|false
232 */
233 private static function try_find_row( $slug, $status, $owner_user_id, &$chain ) {
234
235 $slug = sanitize_text_field( (string) $slug );
236 $status = sanitize_key( (string) $status );
237 $owner_user_id = max( 0, (int) $owner_user_id );
238
239 $chain[] = array(
240 'form_slug' => $slug,
241 'status' => $status,
242 'owner_user_id' => $owner_user_id,
243 );
244 // Here we already have the booking form configuration: row.advanced_form = "<div>..." also row.content_form = '...', row.settings_json = '{...}'
245 $row = WPBC_BFB_Form_Storage::get_form_row_by_key( $slug, $status, $owner_user_id );
246
247 if ( ! $row || empty( $row->booking_form_id ) ) {
248 return false;
249 }
250
251 return array(
252 'form_id' => (int) $row->booking_form_id,
253 'form_slug' => $slug,
254 'status' => $status,
255 'owner_user_id' => isset( $row->owner_user_id ) ? max( 0, (int) $row->owner_user_id ) : $owner_user_id,
256 );
257 }
258
259 /**
260 * Controlled missing BFB result.
261 *
262 * @param string $reason Missing reason.
263 * @param array $fallback_chain Resolution attempts.
264 *
265 * @return array
266 */
267 private static function missing_bfb_result( $reason, $fallback_chain = array() ) {
268 return array(
269 'engine' => 'bfb_missing',
270 'reason' => (string) $reason,
271 'apply_after_load_filter' => false,
272 'bfb_loader_args' => array(),
273 'fallback_chain' => is_array( $fallback_chain ) ? $fallback_chain : array(),
274 );
275 }
276 }
277
278
279 /**
280 * BFB resolver for "Booking Form Data" template (content_form).
281 *
282 * Purpose:
283 * - When BFB DB engine is used for a form, the correct booking "data template"
284 * must come from wp_booking_form_structures.content_form (not booking_form_show option).
285 * - This is used by wpbc_get__booking_form_data__show() to replace [field] shortcodes
286 * with values from booking form_data string.
287 *
288 * Design notes:
289 * - Uses WPBC_FE_Form_Source_Resolver::resolve() to keep the same decision chain as front-end rendering.
290 * - Reads the template via wpbc_bfb_get_booking_form_pair() (must expose content_form).
291 * - Safe no-op when BFB / resolver is not available.
292 *
293 * @since 11.0.x
294 */
295 class WPBC_BFB_Booking_Data_Content_Resolver {
296
297 /**
298 * Try to override legacy $booking_form_show by BFB content_form (resolver-driven).
299 *
300 * @param string $booking_form_show Legacy resolved booking_form_show.
301 * @param int $resource_id Booking resource ID.
302 * @param string $form_slug Form slug/name (legacy: 'standard' or custom form name).
303 * @param string $form_data Booking form_data string from DB.
304 * @param array $params Optional. Context override (e.g. form_status/preview token).
305 *
306 * @return string Possibly overridden template.
307 */
308 public static function maybe_override_booking_form_show_by_bfb( $booking_form_show, $resource_id, $form_slug, $form_data, $params = array() ) {
309
310 $booking_form_show = (string) $booking_form_show;
311 $resource_id = (int) $resource_id;
312 $form_slug = (string) $form_slug;
313 $form_data = (string) $form_data;
314
315 if ( '' === $form_slug ) {
316 $form_slug = 'standard';
317 }
318
319 // Hard requirements for BFB resolver path.
320 if ( ! class_exists( 'WPBC_FE_Form_Source_Resolver' ) ) {
321 return $booking_form_show;
322 }
323 if ( ! class_exists( 'WPBC_BFB_Form_Loader' ) ) {
324 return $booking_form_show;
325 }
326
327 // ------------------------------------------------------------
328 // Preview-aware context (critical fix).
329 // ------------------------------------------------------------
330 $ctx = array();
331 if ( function_exists( 'wpbc_get_request_form_context' ) ) {
332 $ctx = wpbc_get_request_form_context();
333 }
334 $ctx = wp_parse_args( ( is_array( $params ) ? $params : array() ), ( is_array( $ctx ) ? $ctx : array() ) );
335
336 $form_status = ( isset( $ctx['form_status'] ) ) ? sanitize_key( $ctx['form_status'] ) : 'published';
337 if ( in_array( $form_status, array( 'publish', 'published' ), true ) ) {
338 $form_status = 'published';
339 }
340 if ( 'preview' !== $form_status ) {
341 $form_status = 'published';
342 }
343
344 // If preview session has exported content_form payload => use it (fast + correct).
345 if ( ( 'preview' === $form_status ) && function_exists( 'wpbc_bfb_preview__maybe_get_payload_from_context' ) ) {
346
347 $payload = wpbc_bfb_preview__maybe_get_payload_from_context( $ctx );
348
349 if ( ! empty( $payload ) && isset( $payload['content_form'] ) && ( '' !== (string) $payload['content_form'] ) ) {
350
351 $content_form = (string) $payload['content_form'];
352
353 // Keep legacy behavior: custom html shortcodes + language.
354 $content_form = wpbc_bf__replace_custom_html_shortcodes( $content_form );
355 $content_form = wpbc_lang( $content_form );
356
357 return $content_form;
358 }
359 }
360
361 // ------------------------------------------------------------
362 // Resolver-driven DB load (status must NOT be hard-coded).
363 // ------------------------------------------------------------
364 $custom_params = array();
365
366 // Optional: allow future-proof explicit form_id embedded into booking form_data.
367 $bfb_form_id = self::get_bfb_form_id_from_form_data( $form_data, $resource_id );
368 if ( $bfb_form_id > 0 ) {
369 $custom_params['bfb_form_id'] = (int) $bfb_form_id;
370 }
371
372 $req = array(
373 'resource_id' => $resource_id,
374 'form_slug' => $form_slug,
375 'form_status' => $form_status, // <-- critical: use preview/published
376 'custom_params' => $custom_params,
377 // Future-proof: let filters access the full preview context if they need it.
378 'ctx' => $ctx,
379 );
380
381 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
382
383 // If preview status is requested but nothing found, try published as fallback (keeps BFB).
384 if ( ( empty( $resolved['engine'] ) || ( 'bfb_db' !== $resolved['engine'] ) ) && ( 'preview' === $form_status ) ) {
385 $req['form_status'] = 'published';
386 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
387 }
388
389 if ( empty( $resolved['engine'] ) || ( 'bfb_db' !== $resolved['engine'] ) ) {
390 return $booking_form_show;
391 }
392
393 $bfb_loader_args = array();
394 if ( ! empty( $resolved['bfb_loader_args'] ) && is_array( $resolved['bfb_loader_args'] ) ) {
395 $bfb_loader_args = $resolved['bfb_loader_args'];
396 }
397
398 // Keep old behavior: allow explicit override from booking data (if present).
399 if ( ( $bfb_form_id > 0 ) && empty( $bfb_loader_args['form_id'] ) ) {
400 $bfb_loader_args['form_id'] = (int) $bfb_form_id;
401 }
402
403 // Signal loader that we need the "content_form" (booking fields data template).
404 $bfb_loader_args['return'] = 'content_form';
405
406 $bfb_pair = wpbc_bfb_get_booking_form_pair( $bfb_loader_args );
407
408 $content_form = self::extract_content_form_from_pair( $bfb_pair );
409
410 if ( '' === trim( $content_form ) ) {
411 return $booking_form_show;
412 }
413
414 // Keep legacy behavior: custom html shortcodes + language.
415 $content_form = wpbc_bf__replace_custom_html_shortcodes( $content_form );
416 $content_form = wpbc_lang( $content_form );
417
418 return $content_form;
419 }
420
421
422 /**
423 * Extract content template from loader return (supports multiple formats).
424 *
425 * Supported formats:
426 * 1) array( 'content' => '...' ) <- current WPBC_BFB_Form_Loader output
427 * 2) array( 'content_form' => '...' ) <- optional future alias
428 * 3) array( 'form' => array( 'content' => '...' ) ) <- future-proof
429 * 4) array( 'form' => array( 'content_form' => '...' ) ) <- future-proof
430 *
431 * @param mixed $bfb_pair Loader result.
432 *
433 * @return string
434 */
435 private static function extract_content_form_from_pair( $bfb_pair ) {
436
437 if ( ! is_array( $bfb_pair ) ) {
438 return '';
439 }
440
441 /**
442 * Safety: if loader fell back to legacy, do NOT override booking_form_show.
443 * WPBC_BFB_Form_Loader returns 'source' => 'builder'|'legacy'.
444 */
445 if ( isset( $bfb_pair['source'] ) && ( 'builder' !== $bfb_pair['source'] ) ) {
446 return '';
447 }
448
449 // Current loader output key.
450 if ( isset( $bfb_pair['content'] ) && is_string( $bfb_pair['content'] ) ) {
451 return (string) $bfb_pair['content'];
452 }
453
454 // Optional alias (if some implementations already return it).
455 if ( isset( $bfb_pair['content_form'] ) && is_string( $bfb_pair['content_form'] ) ) {
456 return (string) $bfb_pair['content_form'];
457 }
458
459 // Future-proof nested formats.
460 if ( isset( $bfb_pair['form'] ) && is_array( $bfb_pair['form'] ) ) {
461
462 if ( isset( $bfb_pair['form']['content'] ) && is_string( $bfb_pair['form']['content'] ) ) {
463 return (string) $bfb_pair['form']['content'];
464 }
465
466 if ( isset( $bfb_pair['form']['content_form'] ) && is_string( $bfb_pair['form']['content_form'] ) ) {
467 return (string) $bfb_pair['form']['content_form'];
468 }
469 }
470
471 return '';
472 }
473
474
475 /**
476 * (Optional / future-proof) Parse BFB form_id stored inside booking form_data.
477 *
478 * Supported patterns:
479 * - "wpbc_bfb_form_id{resource_id}^{ID}"
480 * - "wpbc_bfb_form_id^{ID}"
481 *
482 * @param string $form_data
483 * @param int $resource_id
484 *
485 * @return int
486 */
487 private static function get_bfb_form_id_from_form_data( $form_data, $resource_id ) {
488
489 $form_data = (string) $form_data;
490 $resource_id = (int) $resource_id;
491
492 if ( '' === $form_data ) {
493 return 0;
494 }
495
496 $prefixes = array(
497 'wpbc_bfb_form_id' . $resource_id . '^',
498 'wpbc_bfb_form_id^',
499 );
500
501 foreach ( $prefixes as $prefix ) {
502
503 $pos = strpos( $form_data, $prefix );
504 if ( false === $pos ) {
505 continue;
506 }
507
508 $chunk = substr( $form_data, $pos + strlen( $prefix ) );
509
510 // Ends at "~" if present.
511 if ( false !== strpos( $chunk, '~' ) ) {
512 $chunk = substr( $chunk, 0, strpos( $chunk, '~' ) );
513 }
514
515 $chunk = trim( $chunk );
516 if ( '' === $chunk ) {
517 continue;
518 }
519
520 $form_id = (int) $chunk;
521 if ( $form_id > 0 ) {
522 return $form_id;
523 }
524 }
525
526 return 0;
527 }
528 }
529