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 / page-form-builder / save-load / bfb-form-manager.php

bfb-form-manager.php in Booking Calendar 11.2, at includes/page-form-builder/save-load/bfb-form-manager.php

732 lines 26.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit; // Exit if accessed directly.
4 }
5
6 /**
7 * Helper functions for saving / loading between different versions of booking forms.
8 *
9 * This file is the bridge between:
10 *
11 * - Old world: booking_form, booking_form_show, booking_forms_extended, etc.
12 * - New world: booking_form_structures table.
13 *
14 * It introduces a normalized FormConfig array:
15 *
16 * [
17 * 'id' => booking_form_id,
18 * 'form_name' => 'standard' or custom key (maps to form_slug),
19 * 'engine' => 'bfb' | 'legacy_advanced' | 'legacy_simple',
20 * 'engine_version' => '1.0',
21 * 'title' => string,
22 * 'description' => string,
23 * 'scope' => string,
24 * 'status' => string,
25 * 'is_default' => int (0|1),
26 * 'booking_resource_id' => int|null,
27 * 'owner_user_id' => int,
28 * 'structure_json' => string, // BFB structure or legacy stub/simple_form.
29 * 'settings' => array, // Decoded settings_json.
30 * 'advanced_form' => string, // Booking form shortcode configuration.
31 * 'content_form' => string, // Fields content template.
32 * 'picture_url' => string,
33 * 'created_at' => string,
34 * 'updated_at' => string,
35 * ]
36 *
37 * What we do:
38 * - Re-use booking_form_structures as canonical FormConfig storage.
39 * Each row now carries:
40 * - form_slug (your form_name): 'standard', custom names, etc.
41 * - owner_user_id for MultiUser ownership.
42 * - engine, engine_version.
43 * - structure_json (BFB structure, or legacy stub/simple_form).
44 * - advanced_form + content_form (what front-end uses today).
45 *
46 * Main public API:
47 * - wpbc_form_config_load():
48 * Tries the booking_form_structures table first, falls back to legacy
49 * wp_options-based configuration if no row exists.
50 *
51 * - wpbc_form_config_save():
52 * Used by the new Builder save controller. Writes to
53 * booking_form_structures via WPBC_BFB_Form_Storage::save_form().
54 * Optionally syncs booking_form / booking_forms_extended so all old
55 * runtime code keeps working.
56 *
57 * @package Booking Calendar.
58 * @author wpdevelop, oplugins
59 * @web-site https://wpbookingcalendar.com/
60 * @email info@wpbookingcalendar.com
61 *
62 * @modified 2025-12-07
63 * @version 1.1
64 * @file ../includes/page-form-builder/save-load/bfb-form-manager.php
65 */
66
67 // == 1. Small JSON helper ==
68
69 /**
70 * Safely JSON-encode arbitrary data for storage.
71 *
72 * Wrapper around wp_json_encode() that guarantees a string return value.
73 * If encoding fails, an empty string is returned.
74 *
75 * @param mixed $data Arbitrary data to be encoded.
76 *
77 * @return string JSON string, or empty string on failure.
78 */
79 function wpbc_form_config__encode_json( $data ) {
80 $json = wp_json_encode( $data );
81
82 return ( false === $json ) ? '' : $json;
83 }
84
85 // == 2. Build FormConfig from DB row ==
86
87 /**
88 * Normalize raw DB row from booking_form_structures into a FormConfig array.
89 *
90 * This is the single place where columns from booking_form_structures are
91 * mapped into the canonical FormConfig keys used in the new Form Manager.
92 *
93 * @param object $row Database row object. Typically returned by
94 * WPBC_BFB_Form_Storage::get_current_form_by_key().
95 *
96 * @return array|null Normalized FormConfig array or null if the row is empty.
97 */
98 function wpbc_form_config__from_row( $row ) {
99
100 if ( ! $row ) {
101 return null;
102 }
103
104 $settings = array();
105 if ( ! empty( $row->settings_json ) ) {
106 $decoded = json_decode( $row->settings_json, true );
107 if ( is_array( $decoded ) ) {
108 $settings = $decoded;
109 }
110 }
111
112 return array(
113 'id' => (int) $row->booking_form_id,
114 'owner_user_id' => isset( $row->owner_user_id ) ? (int) $row->owner_user_id : 0,
115 // External API uses "form_name" (key used in Builder / shortcodes); in DB it is stored as form_slug.
116 'form_name' => isset( $row->form_slug ) ? (string) $row->form_slug : '',
117 'engine' => ! empty( $row->engine ) ? (string) $row->engine : 'bfb',
118 'engine_version' => ! empty( $row->engine_version ) ? (string) $row->engine_version : '1.0',
119 'title' => isset( $row->title ) ? (string) $row->title : '',
120 'description' => isset( $row->description ) ? (string) $row->description : '',
121 'scope' => isset( $row->scope ) ? (string) $row->scope : 'global',
122 'status' => isset( $row->status ) ? (string) $row->status : 'published',
123 'is_default' => isset( $row->is_default ) ? (int) $row->is_default : 0,
124 'booking_resource_id' => isset( $row->booking_resource_id ) ? (int) $row->booking_resource_id : null,
125 'structure_json' => isset( $row->structure_json ) ? (string) $row->structure_json : '',
126 'settings' => $settings,
127 'advanced_form' => isset( $row->advanced_form ) ? (string) $row->advanced_form : '',
128 'content_form' => isset( $row->content_form ) ? (string) $row->content_form : '',
129 'picture_url' => isset( $row->picture_url ) ? (string) $row->picture_url : '',
130 'created_at' => isset( $row->created_at ) ? (string) $row->created_at : '',
131 'updated_at' => isset( $row->updated_at ) ? (string) $row->updated_at : '',
132 );
133 }
134
135 // == 3. Detect if a form already exists in storage ==
136
137 /**
138 * Check if a FormConfig row already exists in storage.
139 *
140 * Currently this lookup is global per site (form_slug only, status 'published')
141 * and does not yet filter by owner_user_id. The user_id parameter is reserved
142 * for future MultiUser-aware lookups.
143 *
144 * @param string $form_key Unique form key/slug (e.g. 'standard').
145 * @param int $user_id Optional. Owner user ID. Default 0 (global, not used yet).
146 *
147 * @return bool True if a row exists, false otherwise.
148 */
149 function wpbc_form_config_exists_in_storage( $form_key, $user_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
150
151 $form_key = (string) $form_key;
152 if ( '' === $form_key ) {
153 return false;
154 }
155
156 // Do not filter by owner_user_id for now – global per site, status = 'published'.
157 $row = WPBC_BFB_Form_Storage::get_current_form_by_key( $form_key, $user_id, 'published' );
158
159 return ( $row && ! empty( $row->booking_form_id ) );
160 }
161
162
163 // == 4. Legacy loader (fallback from old options) ==
164
165 /**
166 * Build FormConfig from legacy options when no row exists in booking_form_structures.
167 *
168 * Behaviour:
169 * - Standard form uses:
170 * - booking_form (advanced form).
171 * - booking_form_show (content form).
172 * - booking_form_visual (Simple form structure, if present).
173 * - Custom forms use booking_forms_extended (serialized array of forms).
174 *
175 * Returned FormConfig uses:
176 * - engine: 'legacy_advanced' or 'legacy_simple' depending on available data.
177 * - structure_json: encoded visual/simple_form structure or legacy stub.
178 *
179 * @param string $form_key Form key. 'standard' or custom key used in legacy options.
180 * @param int $user_id Owner user ID for bookkeeping. Default 0.
181 *
182 * @return array|null FormConfig array on success or null if no legacy form found.
183 */
184 function wpbc_form_config__load_from_legacy( $form_key, $user_id = 0 ) {
185
186 $form_key = (string) $form_key;
187 if ( '' === $form_key ) {
188 $form_key = wpbc_bfb_get_default_form_key();
189 }
190
191 // 1) Standard form.
192 if ( 'standard' === $form_key ) {
193
194 $engine = 'legacy_advanced';
195 $advanced_form = wpbc_bfb_get_legacy_option_value( 'booking_form', $user_id );
196 $content_form = wpbc_bfb_get_legacy_option_value( 'booking_form_show', $user_id );
197
198 $visual = wpbc_bfb_get_legacy_option_value( 'booking_form_visual', $user_id );
199 $visual = maybe_unserialize( $visual );
200
201 $structure_json = wpbc_form_config__encode_json( array() );
202
203 // Free version only: use Simple Form structure as fallback Builder structure.
204 if ( ! class_exists( 'wpdev_bk_personal' ) ) {
205 if ( is_array( $visual ) && ! empty( $visual ) ) {
206 $engine = 'legacy_simple';
207 $structure_arr = wpbc_simple_form__export_to_bfb_structure( $visual );
208 $structure_json = wpbc_form_config__encode_json( $structure_arr );
209 }
210 }
211
212 return array(
213 'id' => 0,
214 'owner_user_id' => (int) $user_id,
215 'form_name' => 'standard',
216 'engine' => $engine,
217 'engine_version' => '1.0',
218 'title' => 'Standard',
219 'description' => '',
220 'scope' => 'global',
221 'status' => 'published',
222 'is_default' => 1,
223 'booking_resource_id' => null,
224 'structure_json' => $structure_json,
225 'settings' => array(),
226 'advanced_form' => (string) $advanced_form,
227 'content_form' => (string) $content_form,
228 'picture_url' => '',
229 'created_at' => '',
230 'updated_at' => '',
231 );
232 }
233
234 // 2) Custom forms from booking_forms_extended.
235 $extended = wpbc_bfb_get_legacy_option_value( 'booking_forms_extended', $user_id );
236 $extended = maybe_unserialize( $extended );
237
238 if ( is_array( $extended ) ) {
239 foreach ( $extended as $one ) {
240
241 if ( empty( $one['name'] ) || (string) $one['name'] !== $form_key ) {
242 continue;
243 }
244
245 $adv = isset( $one['form'] ) ? $one['form'] : '';
246 $cnt = isset( $one['content'] ) ? $one['content'] : '';
247 $engine = 'legacy_advanced';
248 $structure_json = wpbc_form_config__encode_json( array() );
249
250 // Only if Advanced Form is absent, or plugin works in Free mode.
251 if ( ( ! class_exists( 'wpdev_bk_personal' ) ) || empty( $adv ) ) {
252 if ( ! empty( $one['simple_form'] ) && is_array( $one['simple_form'] ) ) {
253 $engine = 'legacy_simple';
254 $structure_arr = wpbc_simple_form__export_to_bfb_structure( $one['simple_form'] );
255 $structure_json = wpbc_form_config__encode_json( $structure_arr );
256 }
257 }
258
259 return array(
260 'id' => 0,
261 'owner_user_id' => (int) $user_id,
262 'form_name' => $form_key,
263 'engine' => $engine,
264 'engine_version' => '1.0',
265 'title' => isset( $one['title'] ) ? (string) $one['title'] : (string) $form_key,
266 'description' => isset( $one['description'] ) ? (string) $one['description'] : '',
267 'scope' => 'global',
268 'status' => 'published',
269 'is_default' => ! empty( $one['is_default'] ) ? 1 : 0,
270 'booking_resource_id' => isset( $one['booking_resource_id'] ) ? intval( $one['booking_resource_id'] ) : null,
271 'structure_json' => $structure_json,
272 'settings' => array(),
273 'advanced_form' => (string) $adv,
274 'content_form' => (string) $cnt,
275 'picture_url' => '',
276 'created_at' => '',
277 'updated_at' => '',
278 );
279 }
280 }
281
282 return null;
283 }
284
285 // == 5. Public loader: try DB first, then legacy ==
286
287 /**
288 * Load unified FormConfig for a given form_key and optional owner.
289 *
290 * This is the main read API for booking form configuration:
291 * - First tries the booking_form_structures table (new engine).
292 * Currently we load status = 'published' row via
293 * WPBC_BFB_Form_Storage::get_current_form_by_key().
294 * - If nothing is found, falls back to legacy options via
295 * wpbc_form_config__load_from_legacy().
296 *
297 * @param string $form_key Form key. 'standard' or custom key. Default 'standard'.
298 * @param int $user_id Owner user ID for MultiUser environment. Default 0.
299 *
300 * @return array|null Normalized FormConfig array or null if nothing found.
301 */
302 function wpbc_form_config_load( $form_key = 'standard', $user_id = 0, $status = 'published', $is_fallback_to_legacy = true ) {
303
304 $form_key = (string) $form_key;
305 if ( '' === $form_key ) {
306 $form_key = 'standard';
307
308 }
309
310 // 1) Try storage table first (status = 'published').
311 $row = WPBC_BFB_Form_Storage::get_current_form_by_key( $form_key, $user_id, $status );
312 if ( $row ) {
313 return wpbc_form_config__from_row( $row );
314 }
315
316 // 2) Fallback to legacy options.
317 if ( $is_fallback_to_legacy ) {
318
319 return wpbc_form_config__load_from_legacy( $form_key, $user_id );
320
321 }
322
323 // Not found.
324 return null;
325 }
326
327 // == 6. Sync back to legacy options (for runtime compatibility) ==
328
329 /**
330 * Sync a normalized FormConfig back into legacy wp_options for runtime compatibility.
331 *
332 * This keeps all existing Booking Calendar runtime code working while the
333 * new Form Builder engine is being rolled out.
334 *
335 * Behaviour:
336 * - 'standard' form maps to:
337 * - booking_form (advanced_form).
338 * - booking_form_show (content_form).
339 * - Custom forms are stored in booking_forms_extended (serialized array).
340 *
341 * @param array $cfg FormConfig array produced by the Form Manager.
342 *
343 * @return void
344 */
345 function wpbc_form_config__sync_legacy_options( array $cfg ) {
346
347 if ( empty( $cfg['form_name'] ) ) {
348 return;
349 }
350
351 $form_name = (string) $cfg['form_name'];
352 $advanced_form = isset( $cfg['advanced_form'] ) ? (string) $cfg['advanced_form'] : '';
353 $content_form = isset( $cfg['content_form'] ) ? (string) $cfg['content_form'] : '';
354
355 // Standard form.
356 if ( 'standard' === $form_name ) {
357 if ( '' !== $advanced_form ) {
358 update_bk_option( 'booking_form', $advanced_form );
359 }
360 if ( '' !== $content_form ) {
361 update_bk_option( 'booking_form_show', $content_form );
362 }
363
364 // Optionally update booking_form_visual from structure_json for legacy_simple
365 // (we can add this later when exact mapping is defined).
366 return;
367 }
368
369 // Custom forms: booking_forms_extended.
370 $extended = get_bk_option( 'booking_forms_extended' );
371 $extended = maybe_unserialize( $extended );
372
373 if ( ! is_array( $extended ) ) {
374 $extended = array();
375 }
376
377 $found = false;
378
379 foreach ( $extended as &$one ) {
380 if ( empty( $one['name'] ) || (string) $one['name'] !== $form_name ) {
381 continue;
382 }
383
384 $one['form'] = $advanced_form;
385 $one['content'] = $content_form;
386 $found = true;
387 break;
388 }
389 unset( $one );
390
391 if ( ! $found ) {
392 $extended[] = array(
393 'name' => $form_name,
394 'form' => $advanced_form,
395 'content' => $content_form,
396 );
397 }
398
399 update_bk_option( 'booking_forms_extended', serialize( $extended ) );
400 }
401
402 // == 7. Public saver: BFB (and later other engines) -> DB + legacy ==
403
404 /**
405 * Save BFB Form into DB - Main Save Function!
406 *
407 * Save unified FormConfig into booking_form_structures and sync legacy options.
408 *
409 * Expected minimal keys in $form_config:
410 * - form_name
411 * - engine
412 * - structure_json (for BFB this is full Builder structure JSON)
413 * - advanced_form (booking form shortcode configuration)
414 * - content_form (fields data/content configuration)
415 *
416 * Behaviour:
417 * - Always writes to booking_form_structures via WPBC_BFB_Form_Storage::save_form().
418 * - Uses form_slug = form_name.
419 * - Uses status from $form_config['status'] or 'published' by default.
420 * - Optionally calls wpbc_form_config__sync_legacy_options() to keep
421 * existing legacy runtime code working.
422 *
423 * @param array $form_config Normalized FormConfig array.
424 * @param array $args {
425 * Optional. Additional saving arguments.
426 *
427 * @type bool $sync_legacy Whether to sync legacy wp_options. Default true.
428 * }
429 *
430 * @return int|false booking_form_id on success, or false on failure.
431 */
432 function wpbc_form_config_save( array $form_config, array $args = array() ) {
433
434 $args = wp_parse_args(
435 $args,
436 array(
437 'sync_legacy' => true,
438 )
439 );
440
441 $form_name = isset( $form_config['form_name'] ) ? (string) $form_config['form_name'] : '';
442 if ( '' === $form_name ) {
443 $form_name = 'standard';
444 }
445
446 $engine = isset( $form_config['engine'] ) ? (string) $form_config['engine'] : 'bfb';
447 $engine_version = isset( $form_config['engine_version'] ) ? (string) $form_config['engine_version'] : '1.0';
448
449 $structure_json = isset( $form_config['structure_json'] ) ? (string) $form_config['structure_json'] : '';
450 $settings = isset( $form_config['settings'] ) ? $form_config['settings'] : array();
451 $settings_json = wpbc_form_config__encode_json( $settings ); // Returns '' on error/empty.
452
453 $advanced_form = isset( $form_config['advanced_form'] ) ? (string) $form_config['advanced_form'] : '';
454 $content_form = isset( $form_config['content_form'] ) ? (string) $form_config['content_form'] : '';
455
456 if ( '' === $structure_json && 'bfb' === $engine ) {
457 // For BFB we expect real structure JSON.
458 return false;
459 }
460
461 $owner_user_id = isset( $form_config['owner_user_id'] ) ? (int) $form_config['owner_user_id'] : 0;
462
463 $storage_data = array(
464 // New schema uses form_slug; we map form_name -> form_slug.
465 'form_slug' => $form_name,
466
467 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : $form_name,
468 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '',
469 'scope' => isset( $form_config['scope'] ) ? (string) $form_config['scope'] : 'global',
470
471 'booking_resource_id' => isset( $form_config['booking_resource_id'] ) ? (int) $form_config['booking_resource_id'] : null,
472 'owner_user_id' => ! empty( $owner_user_id ) ? $owner_user_id : 0,
473 'is_default' => ! empty( $form_config['is_default'] ) ? 1 : 0,
474 'status' => isset( $form_config['status'] ) ? (string) $form_config['status'] : 'published',
475
476 'engine' => $engine,
477 'engine_version' => $engine_version,
478 'structure_json' => $structure_json,
479 'settings_json' => $settings_json,
480 'advanced_form' => $advanced_form,
481 'content_form' => $content_form,
482
483 // Optional picture/preview image URL.
484 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : null,
485 );
486
487 $booking_form_id = WPBC_BFB_Form_Storage::save_form( $storage_data );
488
489 if ( $booking_form_id && ! empty( $args['sync_legacy'] ) ) {
490 wpbc_form_config__sync_legacy_options( $form_config );
491 }
492
493 return $booking_form_id;
494 }
495
496
497 // == Default Load Form !) ==
498
499 /**
500 * Get default form key used by the Form Builder.
501 *
502 * Centralizes the default key for the main booking form, currently 'standard'.
503 *
504 * @return string Default form key.
505 */
506 function wpbc_bfb_get_default_form_key() {
507
508 if ( isset( $_GET['form_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
509 $form_name = sanitize_text_field( wp_unslash( $_GET['form_name'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
510 if ( '' !== $form_name ) {
511 return $form_name;
512 }
513 }
514
515 return 'standard';
516 }
517
518 /**
519 * Output AJAX boot configuration for the Form Builder.
520 *
521 * Prints a small inline script that exposes WPBC_BFB_Ajax on window. This
522 * configuration is consumed by the JS Builder to perform save / load
523 * requests via admin-ajax.php.
524 *
525 * Should be called only on the Form Builder admin page.
526 *
527 * @return void
528 */
529 function wpbc_bfb_output_ajax_boot_config() {
530
531 $form_key = wpbc_bfb_get_default_form_key(); // 'standard' – or detect from current screen / URL / selection.
532
533 $ajax_config = array(
534 'url' => admin_url( 'admin-ajax.php' ),
535 'nonce_save' => wp_create_nonce( 'wpbc_bfb_form_save' ),
536 'nonce_load' => wp_create_nonce( 'wpbc_bfb_form_load' ),
537 'nonce_create' => wp_create_nonce( 'wpbc_bfb_form_create' ),
538 'nonce_list' => wp_create_nonce( 'wpbc_bfb_form_list' ),
539 'form_name' => $form_key, // info: INIT_FORM_LOAD.
540 'engine' => 'bfb',
541 'engine_version' => '1.0',
542 // Initial load behavior.
543 'initial_load' => 'ajax', // 'ajax' / 'example' / 'blank'.
544 'initial_load_fallback' => 'example', // 'example' / 'blank'.
545 'load_action' => 'WPBC_AJX_BFB_LOAD_FORM_CONFIG',
546 );
547 ?>
548 <script type="text/javascript"> window.WPBC_BFB_Ajax = <?php echo wp_json_encode( $ajax_config ); ?>; </script>
549 <?php
550 }
551
552 /**
553 * Get the "advanced" booking form configuration for a given form key.
554 *
555 * This is a thin convenience wrapper around wpbc_form_config_load() that
556 * returns only the advanced_form string.
557 *
558 * @param string $form_key Form key. Default 'standard'.
559 *
560 * @return string Advanced form configuration or empty string if not found.
561 */
562 function wpbc_get_form_advanced( $form_key = 'standard' ) {
563 $cfg = wpbc_form_config_load( $form_key );
564
565 return ( ! empty( $cfg['advanced_form'] ) ? $cfg['advanced_form'] : '' );
566 }
567
568 /**
569 * Get the "content" (fields data) configuration for a given form key.
570 *
571 * This is a thin convenience wrapper around wpbc_form_config_load() that
572 * returns only the content_form string.
573 *
574 * @param string $form_key Form key. Default 'standard'.
575 *
576 * @return string Content form configuration or empty string if not found.
577 */
578 function wpbc_get_form_content( $form_key = 'standard' ) {
579 $cfg = wpbc_form_config_load( $form_key );
580
581 return ( ! empty( $cfg['content_form'] ) ? $cfg['content_form'] : '' );
582 }
583
584
585
586 /**
587 * Bridge: implement wpbc_bfb_form_loader_from_builder filter using
588 * booking_form_structures table.
589 *
590 * This function is responsible ONLY for pulling already exported
591 * shortcodes/markup from the BFB storage:
592 * - advanced_form -> "booking form" shortcodes.
593 * - content_form -> "Content of booking fields data" shortcodes.
594 *
595 * It does NOT try to interpret structure_json; that is the Builder's job.
596 *
597 * @since 11.0.0
598 */
599
600
601 /**
602 * Load form/content from booking_form_structures for the Form Loader.
603 *
604 * Expected $args keys (normalized by WPBC_BFB_Form_Loader):
605 * - form_slug : string Logical form key (e.g. 'standard').
606 * - form_id : int booking_form_id (optional, takes precedence).
607 * - status : string 'published', 'preview', 'draft', ...
608 * - resource_id : int Booking resource ID (currently informational).
609 * - user_id : int Current user ID (reserved for future MultiUser).
610 *
611 * @param array $empty Default empty pair: ['form' => '', 'content' => ''].
612 * @param array $args Loader arguments.
613 *
614 * @return array Pair ['form' => string, 'content' => string].
615 */
616 function wpbc_bfb__load_from_bfb_table( $empty, $args ) {
617
618 global $wpdb;
619
620 // Ensure we have a consistent empty array.
621 if ( ! is_array( $empty ) ) {
622 $empty = array(
623 'form' => '',
624 'content' => '',
625 );
626 }
627
628 // Normalized args (defensive).
629 $form_id = isset( $args['form_id'] ) ? intval( $args['form_id'] ) : 0;
630 $form_slug = isset( $args['form_slug'] ) ? (string) $args['form_slug'] : '';
631 $status = isset( $args['status'] ) ? strtolower( trim( (string) $args['status'] ) ) : 'published';
632
633 // FixIn: 2026-03-08.
634 $has_user_key = array_key_exists( 'user_id', $args );
635 $owner_user_id = $has_user_key ? max( 0, (int) $args['user_id'] ) : 0;
636
637 $has_owner_key = array_key_exists( 'owner_user_id', $args );
638 $owner_user_id = $has_owner_key ? max( $owner_user_id, (int) $args['owner_user_id'] ) : $owner_user_id;
639
640 $has_owner_key = $has_user_key || $has_owner_key;
641
642 // -----------------------------------------------------------------
643 // Normalize status to match DB semantics.
644 // booking_form_structures uses values like: active, preview, draft, archived.
645 // -----------------------------------------------------------------
646 if ( '' === $status ) {
647 $status = 'published';
648 } elseif ( in_array( $status, array( 'publish', 'published' ), true ) ) {
649 $status = 'published';
650 }
651
652
653 // Nothing to do if we have neither an ID nor a slug.
654 if ( $form_id <= 0 && '' === $form_slug ) {
655 return $empty;
656 }
657
658 // Optional: if helper exists, ensure table really exists.
659 if ( function_exists( 'wpbc_is_table_exists' ) && ! wpbc_is_table_exists( 'booking_form_structures' ) ) {
660 return $empty;
661 }
662
663 // -----------------------------------------------------------------
664 // Build SELECT depending on whether form_id is available.
665 // -----------------------------------------------------------------
666 if ( $form_id > 0 ) {
667 // Prefer direct lookup by primary key + status.
668 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
669 $row = $wpdb->get_row( $wpdb->prepare( "SELECT advanced_form, content_form, settings_json
670 FROM {$wpdb->prefix}booking_form_structures
671 WHERE booking_form_id = %d
672 AND status = %s
673 LIMIT 1", $form_id, $status ) );
674 } else {
675
676 if ( $has_owner_key && $owner_user_id > 0 ) {
677
678 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
679 $row = $wpdb->get_row( $wpdb->prepare( "SELECT advanced_form, content_form, settings_json
680 FROM {$wpdb->prefix}booking_form_structures
681 WHERE form_slug = %s
682 AND status = %s
683 AND owner_user_id = %d
684 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
685 LIMIT 1", $form_slug, $status, $owner_user_id ) );
686
687 } elseif ( $has_owner_key ) {
688
689 // global: 0 or NULL
690 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
691 $row = $wpdb->get_row( $wpdb->prepare( "SELECT advanced_form, content_form, settings_json
692 FROM {$wpdb->prefix}booking_form_structures
693 WHERE form_slug = %s
694 AND status = %s
695 AND ( owner_user_id = 0 OR owner_user_id IS NULL )
696 ORDER BY version DESC, updated_at DESC, booking_form_id DESC
697 LIMIT 1", $form_slug, $status ) );
698
699 } else {
700
701 // Legacy behavior: no owner filter at all.
702 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
703 $row = $wpdb->get_row( $wpdb->prepare( "SELECT advanced_form, content_form, settings_json
704 FROM {$wpdb->prefix}booking_form_structures
705 WHERE form_slug = %s
706 AND status = %s
707 LIMIT 1", $form_slug, $status ) );
708 }
709 }
710
711
712 if ( ! $row ) {
713 return $empty;
714 }
715
716 // Ensure we always return both keys, even if one is empty.
717 $form = isset( $row->advanced_form ) ? (string) $row->advanced_form : '';
718 $content = isset( $row->content_form ) ? (string) $row->content_form : '';
719 $settings_json = isset( $row->settings_json ) ? (string) $row->settings_json : '';
720
721 return array(
722 'form' => $form,
723 'content' => $content,
724 'settings_json' => $settings_json,
725 );
726 }
727
728 /**
729 * Attach loader to wpbc_bfb_form_loader_from_builder.
730 */
731 add_filter( 'wpbc_bfb_form_loader_from_builder', 'wpbc_bfb__load_from_bfb_table', 10, 2 );
732