PluginProbe
Booking Calendar / 11.4.3
Booking Calendar v11.4.3
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 / page-form-builder / save-load / bfb-activate.php

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

1,237 lines 36.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Form Builder - Activation helpers.
4 *
5 * Responsibilities:
6 * 1) Create BFB DB table (wp_booking_form_structures) on activation (if missing).
7 * 2) Ensure the "Booking Form Preview" page exists during activation.
8 *
9 * @package Booking Calendar.
10 * @author wpdevelop, oplugins
11 * @web-site https://wpbookingcalendar.com/
12 * @email info@wpbookingcalendar.com
13 *
14 * @since 11.0.x
15 * @file ../includes/page-form-builder/save-load/bfb-activate.php
16 * @modified 2026-02-23
17 */
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit; // Exit if accessed directly.
21 }
22
23 /**
24 * Return option key where preview page ID is stored.
25 *
26 * Must match WPBC_BFB_Preview_Service::get_page_option_key().
27 *
28 * @return string
29 */
30 function wpbc_bfb_activation__get_preview_page_option_key() {
31 return 'wpbc_bfb_preview_page_id';
32 }
33
34 /**
35 * Return meta key used to mark the preview page (for recovery).
36 *
37 * @return string
38 */
39 function wpbc_bfb_activation__get_preview_page_meta_key() {
40 return '_wpbc_bfb_preview_page';
41 }
42
43 /**
44 * Return canonical slug for the preview page.
45 *
46 * @return string
47 */
48 function wpbc_bfb_activation__get_preview_page_slug() {
49 return 'wpbc-bfb-preview';
50 }
51
52 /**
53 * Try to find an administrator user ID (best-effort).
54 *
55 * @return int Admin user ID, or 0 if not found.
56 */
57 function wpbc_bfb_activation__get_any_admin_user_id() {
58
59 $users = get_users( array(
60 'role' => 'administrator',
61 'number' => 1,
62 'fields' => array( 'ID' ),
63 ) );
64
65 if ( empty( $users ) ) {
66 return 0;
67 }
68
69 if ( is_object( $users[0] ) && isset( $users[0]->ID ) ) {
70 return (int) $users[0]->ID;
71 }
72
73 if ( is_array( $users[0] ) && isset( $users[0]['ID'] ) ) {
74 return (int) $users[0]['ID'];
75 }
76
77 return 0;
78 }
79
80 /**
81 * Temporarily switch current user to an admin user if the current user
82 * cannot create private pages (activation edge-cases like WP-CLI).
83 *
84 * @return int Previous user ID (0 if none). Caller should restore with wp_set_current_user().
85 */
86 function wpbc_bfb_activation__maybe_switch_to_admin_user() {
87
88 $prev_user_id = (int) get_current_user_id();
89
90 // If current user can create pages, do nothing.
91 if ( $prev_user_id > 0 && user_can( $prev_user_id, 'publish_pages' ) ) {
92 return $prev_user_id;
93 }
94
95 $admin_id = wpbc_bfb_activation__get_any_admin_user_id();
96 if ( $admin_id > 0 ) {
97 wp_set_current_user( $admin_id );
98 }
99
100 return $prev_user_id;
101 }
102
103 /**
104 * Restore previous user after temporary switch.
105 *
106 * @param int $prev_user_id Previous user ID.
107 *
108 * @return void
109 */
110 function wpbc_bfb_activation__restore_user( $prev_user_id ) {
111
112 $prev_user_id = (int) $prev_user_id;
113
114 // Restore to previous user if we had one; otherwise, reset to 0.
115 if ( $prev_user_id > 0 ) {
116 wp_set_current_user( $prev_user_id );
117 } else {
118 wp_set_current_user( 0 );
119 }
120 }
121
122 /**
123 * Validate that a page ID is a real WP page.
124 *
125 * @param int $page_id Page ID.
126 *
127 * @return bool
128 */
129 function wpbc_bfb_activation__is_valid_page_id( $page_id ) {
130
131 $page_id = (int) $page_id;
132
133 if ( $page_id <= 0 ) {
134 return false;
135 }
136
137 $page = get_post( $page_id );
138
139 return ( $page instanceof WP_Post && 'page' === $page->post_type );
140 }
141
142 /**
143 * Try to locate an existing preview page by meta marker.
144 *
145 * @return int Page ID or 0.
146 */
147 function wpbc_bfb_activation__find_preview_page_by_meta() {
148
149 $meta_key = wpbc_bfb_activation__get_preview_page_meta_key();
150
151 $q = new WP_Query( array(
152 'post_type' => 'page',
153 'post_status' => array( 'private', 'publish', 'draft', 'pending' ),
154 'posts_per_page' => 1,
155 'fields' => 'ids',
156 'no_found_rows' => true,
157 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters
158 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
159 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
160 ) );
161
162 if ( empty( $q->posts ) ) {
163 return 0;
164 }
165
166 return (int) $q->posts[0];
167 }
168
169 /**
170 * Try to locate an existing preview page by slug.
171 *
172 * @return int Page ID or 0.
173 */
174 function wpbc_bfb_activation__find_preview_page_by_slug() {
175
176 $slug = wpbc_bfb_activation__get_preview_page_slug();
177
178 $page = get_page_by_path( $slug, OBJECT, 'page' );
179 if ( $page instanceof WP_Post ) {
180 return (int) $page->ID;
181 }
182
183 return 0;
184 }
185
186
187 /**
188 * Force a safe template for the BFB Preview page.
189 *
190 * Why:
191 * - The Preview page is a technical/service page.
192 * - It should not inherit decorative page templates like "Page with wide Image".
193 * - For block themes, prefer a clean template like "page-no-title" or "page".
194 * - For classic themes, use the default page template. // FixIn: 10.15.2.1
195 *
196 * @param int $page_id Page ID.
197 *
198 * @return bool
199 */
200 function wpbc_bfb_preview__force_safe_template( $page_id ) {
201
202 $page_id = absint( $page_id );
203
204 if ( $page_id <= 0 ) {
205 return false;
206 }
207
208 /*
209 * Always clear any previously assigned template first.
210 * This is important when an existing Preview page was previously using
211 * an unwanted template, for example "Page with wide Image".
212 */
213 delete_post_meta( $page_id, '_wp_page_template' );
214
215 // Block themes: prefer a cleaner built-in page template, if available.
216 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && function_exists( 'get_block_templates' ) ) {
217
218 $theme_slug = get_stylesheet();
219 $templates = get_block_templates(
220 array(
221 'post_type' => 'page',
222 ),
223 'wp_template'
224 );
225 $safe_template_slug = '';
226
227 foreach ( $templates as $template ) {
228
229 if ( empty( $template->slug ) || empty( $template->theme ) ) {
230 continue;
231 }
232
233 if ( $theme_slug !== $template->theme ) {
234 continue;
235 }
236
237 if ( 'page-no-title' === $template->slug ) {
238 $safe_template_slug = 'page-no-title';
239 break;
240 }
241
242 if ( ( '' === $safe_template_slug ) && ( 'page' === $template->slug ) ) {
243 $safe_template_slug = 'page';
244 }
245 }
246
247 if ( '' !== $safe_template_slug ) {
248 $result = wp_update_post(
249 array(
250 'ID' => $page_id,
251 'page_template' => $safe_template_slug,
252 ),
253 true
254 );
255
256 return ( ! is_wp_error( $result ) );
257 }
258 }
259
260 return true;
261 }
262
263
264 /**
265 * The preview page has status 'publish', because the secure the output by your nonce + cap
266 *
267 * Preview security is already enforced by:
268 *
269 * wpbc_bfb_preview=1 + token + nonce
270 *
271 * is_user_logged_in() + current_user_can( wpbc_bfb_get_manage_cap() )
272 *
273 * So the preview page itself does not need to be private.
274 *
275 * @return false|int
276 */
277 function wpbc_bfb_activation__create_preview_page_raw() {
278
279 $page_data = array(
280 'post_title' => __( 'Booking Form Preview', 'booking' ),
281 'post_content' => '', // Content will be injected dynamically at preview time by bfb-preview.php.
282 'post_status' => 'publish',
283 'post_type' => 'page',
284 'post_name' => wpbc_bfb_activation__get_preview_page_slug(),
285 'comment_status' => 'closed',
286 'ping_status' => 'closed',
287 );
288
289 $page_id = wp_insert_post( $page_data, true );
290
291 if ( ( ! is_wp_error( $page_id ) ) && ( ! empty( $page_id ) ) ) {
292 // Success.
293 wpbc_bfb_preview__force_safe_template( $page_id );
294 } else {
295 if ( is_wp_error( $page_id ) ) {
296 return false;
297 }
298 if ( ! $page_id ) {
299 return false;
300 }
301 $page_id = false;
302 }
303
304 return (int) $page_id;
305 }
306
307
308 /**
309 * Create the "Booking Form Preview" page and store option.
310 *
311 * @return int|false Page ID on success, false on failure.
312 */
313 function wpbc_bfb_activation__create_preview_page() {
314
315 $prev_user_id = wpbc_bfb_activation__maybe_switch_to_admin_user();
316
317 $page_id = wpbc_bfb_activation__create_preview_page_raw();
318
319 // Restore user context.
320 wpbc_bfb_activation__restore_user( $prev_user_id );
321
322 if ( empty( $page_id ) || is_wp_error( $page_id ) ) {
323 return false;
324 }
325
326 $page_id = (int) $page_id;
327
328 // Mark page for recovery if option is deleted later.
329 add_post_meta( $page_id, wpbc_bfb_activation__get_preview_page_meta_key(), '1', true );
330
331 // Store the page ID in option (same key used by preview service).
332 update_option( wpbc_bfb_activation__get_preview_page_option_key(), $page_id );
333
334 return $page_id;
335 }
336
337 /**
338 * Ensure preview page exists (activation-time).
339 *
340 * - Validates stored option page ID.
341 * - If missing, tries to find by meta marker.
342 * - If missing, tries to find by slug.
343 * - If still missing, creates a new private page.
344 *
345 * @return int|false Page ID, or false on failure.
346 */
347 function wpbc_bfb_activation__ensure_preview_page_exists() {
348
349 $option_key = wpbc_bfb_activation__get_preview_page_option_key();
350 $page_id = (int) get_option( $option_key );
351
352 // 1) Option points to a valid page.
353 if ( wpbc_bfb_activation__is_valid_page_id( $page_id ) ) {
354
355 // Enforce publish status (best effort).
356 $page = get_post( $page_id );
357 if ( $page instanceof WP_Post && 'publish' !== $page->post_status ) {
358 wp_update_post( array(
359 'ID' => (int) $page_id,
360 'post_status' => 'publish',
361 ) );
362 }
363
364 // Ensure meta marker exists.
365 wpbc_bfb_preview__force_safe_template( (int) $page_id );
366 add_post_meta( (int) $page_id, wpbc_bfb_activation__get_preview_page_meta_key(), '1', true );
367
368 return (int) $page_id;
369 }
370
371 // 2) Try recovery by meta marker.
372 $found_id = wpbc_bfb_activation__find_preview_page_by_meta();
373 if ( $found_id > 0 && wpbc_bfb_activation__is_valid_page_id( $found_id ) ) {
374 $found_page = get_post( $found_id );
375 if ( $found_page instanceof WP_Post && 'publish' !== $found_page->post_status ) {
376 wp_update_post( array(
377 'ID' => (int) $found_id,
378 'post_status' => 'publish',
379 ) );
380 }
381 wpbc_bfb_preview__force_safe_template( (int) $found_id );
382 update_option( $option_key, (int) $found_id );
383 return (int) $found_id;
384 }
385
386 // 3) Try recovery by slug.
387 $found_id = wpbc_bfb_activation__find_preview_page_by_slug();
388 if ( $found_id > 0 && wpbc_bfb_activation__is_valid_page_id( $found_id ) ) {
389 $found_page = get_post( $found_id );
390 if ( $found_page instanceof WP_Post && 'publish' !== $found_page->post_status ) {
391 wp_update_post( array(
392 'ID' => (int) $found_id,
393 'post_status' => 'publish',
394 ) );
395 }
396 wpbc_bfb_preview__force_safe_template( (int) $found_id );
397 // Mark it as ours (so next time meta lookup works).
398 add_post_meta( (int) $found_id, wpbc_bfb_activation__get_preview_page_meta_key(), '1', true );
399 update_option( $option_key, (int) $found_id );
400 return (int) $found_id;
401 }
402
403 // 4) Create.
404 return wpbc_bfb_activation__create_preview_page();
405 }
406
407 /**
408 * Activation hook entry: ensure the preview page exists.
409 *
410 * @return void
411 */
412 function wpbc_bfb_activation__preview_page() {
413 wpbc_bfb_activation__ensure_preview_page_exists();
414 }
415 add_bk_action( 'wpbc_free_version_activation', 'wpbc_bfb_activation__preview_page' );
416
417
418 /**
419 * A c t i v a t e - Create new DB Table for Booking Form Builder structures.
420 *
421 * This table stores:
422 * - Canonical form builder structure as JSON (structure_json).
423 * - Additional settings as JSON (settings_json).
424 * - Optional precompiled HTML can be added later (not in this schema).
425 * - Ownership via owner_user_id (for MultiUser).
426 *
427 * @return void
428 */
429 function wpbc_bfb_activation__form_structures_table() {
430
431 global $wpdb;
432
433 $charset_collate = ( ! empty( $wpdb->charset ) ) ? 'DEFAULT CHARACTER SET ' . $wpdb->charset : '';
434 $charset_collate .= ( ! empty( $wpdb->collate ) ) ? ' COLLATE ' . $wpdb->collate : '';
435
436 if ( ! wpbc_is_table_exists( 'booking_form_structures' ) ) {
437
438
439
440 $simple_sql = "CREATE TABLE {$wpdb->prefix}booking_form_structures (
441 booking_form_id bigint(20) unsigned NOT NULL auto_increment,
442 form_slug VARCHAR(191) NOT NULL,
443 status VARCHAR(32) NOT NULL,
444 owner_user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
445
446 scope varchar(64) NOT NULL default 'global',
447 version int(11) NOT NULL default 1,
448 booking_resource_id BIGINT UNSIGNED NULL,
449
450 engine varchar(32) NOT NULL default 'bfb',
451 engine_version varchar(16) NOT NULL default '1.0',
452 structure_json LONGTEXT NOT NULL,
453 settings_json LONGTEXT NULL,
454 advanced_form LONGTEXT NULL,
455 content_form LONGTEXT NULL,
456
457 is_default TINYINT(1) NOT NULL DEFAULT 0,
458 title VARCHAR(255) NOT NULL,
459 description TEXT NULL,
460 picture_url text NULL,
461
462 created_at DATETIME NOT NULL,
463 updated_at DATETIME NOT NULL,
464
465 created_by bigint(20) unsigned DEFAULT NULL,
466 updated_by bigint(20) unsigned DEFAULT NULL,
467 PRIMARY KEY (booking_form_id),
468 UNIQUE KEY form_slug_status (form_slug, status, owner_user_id)
469 ) {$charset_collate}";
470
471 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
472 $wpdb->query( $simple_sql );
473
474 do_action( 'wpbc_bfb_activation__form_structures_table__after_create' );
475
476 wpbc_bfb__import_old_booking_forms();
477 } else {
478 do_action( 'wpbc_bfb_activation__form_structures_table__table_already_exists' );
479 }
480 }
481 add_bk_action( 'wpbc_free_version_activation', 'wpbc_bfb_activation__form_structures_table' );
482
483
484 /**
485 * D e a c t i v a t e - Drop Booking Form Builder structures table.
486 *
487 * @return void
488 */
489 function wpbc_bfb_deactivation__form_structures_table() {
490
491 global $wpdb;
492
493
494
495 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
496 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}booking_form_structures" );
497
498 delete_bk_option( 'wpbc_bfb_legacy_import_done_for_pro' );
499 }
500 add_bk_action( 'wpbc_free_version_deactivation', 'wpbc_bfb_deactivation__form_structures_table' );
501
502
503 /**
504 * Import legacy "standard" booking form as initial BFB structure.
505 *
506 * @return void
507 */
508 function wpbc_bfb_is_brand_new_install__before_version_update() {
509
510 $stored_version = get_option( 'booking_version_num', false );
511
512 return ( false === $stored_version );
513 }
514
515 /**
516 * Let the initial standard BFB form follow the global calendar days-selection setting.
517 *
518 * Some bundled templates intentionally override days selection for their own use case.
519 * The installed "standard" form should not carry that template-level override, because
520 * the global Settings > Calendar page is the canonical default for a new site.
521 *
522 * @param array $template_record Form Builder template record.
523 *
524 * @return array
525 */
526 function wpbc_bfb_activation__set_standard_form_default_days_selection( $template_record ) {
527
528 if ( empty( $template_record ) || ! is_array( $template_record ) ) {
529 return $template_record;
530 }
531
532 $settings = array();
533 if ( ! empty( $template_record['settings_json'] ) ) {
534 $decoded = json_decode( (string) $template_record['settings_json'], true );
535 if ( is_array( $decoded ) ) {
536 $settings = $decoded;
537 }
538 }
539
540 if ( empty( $settings['options'] ) || ! is_array( $settings['options'] ) ) {
541 $settings['options'] = array();
542 }
543
544 $settings['options']['booking_type_of_day_selections'] = '';
545
546 $template_record['settings_json'] = function_exists( 'wpbc_form_config__encode_json' )
547 ? wpbc_form_config__encode_json( $settings )
548 : wp_json_encode( $settings );
549
550 return $template_record;
551 }
552
553 /**
554 * Maybe create the initial standard BFB form from a bundled template.
555 *
556 * Used only during brand new plugin activation, before booking_version_num
557 * gets updated. Upgrades keep migrating the existing legacy configuration.
558 *
559 * @return bool
560 */
561 function wpbc_bfb_maybe_create_initial_standard_form_from_template() {
562
563 if ( ! wpbc_bfb_is_brand_new_install__before_version_update() ) {
564 return false;
565 }
566
567 if ( ! class_exists( 'WPBC_BFB_Form_Storage' ) ) {
568 return false;
569 }
570
571 if ( function_exists( 'wpbc_get_bfb_template_record_by_key' ) ) {
572 // Default Booking Form Template - Just after Booking Calendar activation!
573 // $template_record = wpbc_get_bfb_template_record_by_key( 'time_appointments_3_steps_review_with_hints' ); // Time appointments.
574 $template_record = wpbc_get_bfb_template_record_by_key( 'dates_2_columns_sidebar_hints' ); // Full Days - 2 Columns - with dates hints.
575 } else {
576 $template_record = array();
577 }
578
579 if ( empty( $template_record ) || ! is_array( $template_record ) ) {
580 return false;
581 }
582
583 $template_record = wpbc_bfb_activation__set_standard_form_default_days_selection( $template_record );
584
585 $template_record['form_slug'] = 'standard';
586 $template_record['status'] = 'published';
587 $template_record['scope'] = 'global';
588 $template_record['owner_user_id'] = 0;
589 $template_record['booking_resource_id'] = null;
590 $template_record['is_default'] = 1;
591 $template_record['title'] = __( 'Standard', 'booking' );
592 $template_record['description'] = '';
593 $template_record['picture_url'] = isset( $template_record['picture_url'] ) ? (string) $template_record['picture_url'] : '';
594 if ( function_exists( 'wpbc_bfb_resolve_picture_url' ) ) {
595 $template_record['picture_url'] = wpbc_bfb_resolve_picture_url( $template_record['picture_url'] );
596 }
597
598 $booking_form_id = WPBC_BFB_Form_Storage::save_form( $template_record );
599
600 return ! empty( $booking_form_id );
601 }
602
603
604 /**
605 * Get the dedicated bundled template for the current live-demo edition.
606 *
607 * @param int $owner_user_id Optional MultiUser owner ID.
608 *
609 * @return string Template key, or an empty string outside a supported demo.
610 */
611 function wpbc_bfb_live_demo__get_template_key( $owner_user_id = 0 ) {
612
613 if ( ! function_exists( 'wpbc_is_this_demo' ) || ! wpbc_is_this_demo() ) {
614 return '';
615 }
616
617 $owner_user_id = absint( $owner_user_id );
618
619 if ( class_exists( 'wpdev_bk_multiuser' ) ) {
620 return ( $owner_user_id > 0 ) ? 'demo_multiuser_owner_1' : 'demo_multiuser';
621 }
622 if ( class_exists( 'wpdev_bk_biz_l' ) ) {
623 return 'demo_business_large';
624 }
625 if ( class_exists( 'wpdev_bk_biz_m' ) ) {
626 return 'demo_business_medium';
627 }
628 if ( class_exists( 'wpdev_bk_biz_s' ) ) {
629 return 'demo_business_small';
630 }
631 if ( class_exists( 'wpdev_bk_personal' ) ) {
632 return 'demo_personal';
633 }
634
635 return '';
636 }
637
638
639 /**
640 * Get the global Form Style used by the current live-demo edition.
641 *
642 * @return string Form Style preset key.
643 */
644 function wpbc_bfb_live_demo__get_form_style() {
645
646 if ( class_exists( 'wpdev_bk_multiuser' ) || class_exists( 'wpdev_bk_biz_l' ) ) {
647 return 'light_bordered';
648 }
649 if ( class_exists( 'wpdev_bk_biz_m' ) ) {
650 return 'light_soft';
651 }
652 if ( class_exists( 'wpdev_bk_biz_s' ) ) {
653 return 'light_bordered';
654 }
655 if ( class_exists( 'wpdev_bk_personal' ) ) {
656 return 'light_none';
657 }
658
659 return 'light_bordered';
660 }
661
662
663 /**
664 * Provision a live-demo standard form from a real bundled BFB template.
665 *
666 * Normal owner activation is idempotent and preserves an existing form. The
667 * explicit demo reset path may request replacement to restore fixture data.
668 *
669 * @param int $owner_user_id Owner ID, or 0 for the global/Super Admin form.
670 * @param string $template_key Optional explicit bundled template key.
671 * @param bool $replace Whether to replace an existing standard form.
672 *
673 * @return int|false Booking form ID on success, false on failure.
674 */
675 function wpbc_bfb_live_demo__provision_standard_form( $owner_user_id = 0, $template_key = '', $replace = false ) {
676
677 if ( ! function_exists( 'wpbc_is_this_demo' ) || ! wpbc_is_this_demo() ) {
678 return false;
679 }
680 if ( ! class_exists( 'WPBC_BFB_Form_Storage' ) ) {
681 return false;
682 }
683 if ( function_exists( 'wpbc_is_table_exists' ) && ! wpbc_is_table_exists( 'booking_form_structures' ) ) {
684 return false;
685 }
686
687 $owner_user_id = absint( $owner_user_id );
688 $template_key = sanitize_key( (string) $template_key );
689 if ( '' === $template_key ) {
690 $template_key = wpbc_bfb_live_demo__get_template_key( $owner_user_id );
691 }
692 if ( '' === $template_key ) {
693 return false;
694 }
695
696 $existing = WPBC_BFB_Form_Storage::get_current_form_by_key( 'standard', $owner_user_id, 'published' );
697 if ( ! empty( $existing->booking_form_id ) && ! $replace ) {
698 return (int) $existing->booking_form_id;
699 }
700
701 $template_record = function_exists( 'wpbc_get_bfb_template_record_by_key' )
702 ? wpbc_get_bfb_template_record_by_key( $template_key )
703 : array();
704
705 if (
706 empty( $template_record ) ||
707 ! is_array( $template_record ) ||
708 empty( $template_record['structure_json'] ) ||
709 empty( $template_record['engine'] ) ||
710 'bfb' !== (string) $template_record['engine']
711 ) {
712 return false;
713 }
714
715 $template_record['form_slug'] = 'standard';
716 $template_record['status'] = 'published';
717 $template_record['scope'] = ( $owner_user_id > 0 ) ? 'user' : 'global';
718 $template_record['owner_user_id'] = $owner_user_id;
719 $template_record['booking_resource_id'] = null;
720 $template_record['is_default'] = 1;
721 $template_record['title'] = __( 'Standard', 'booking' );
722 $template_record['description'] = '';
723 $template_record['picture_url'] = isset( $template_record['picture_url'] ) ? (string) $template_record['picture_url'] : '';
724
725 if ( function_exists( 'wpbc_bfb_resolve_picture_url' ) ) {
726 $template_record['picture_url'] = wpbc_bfb_resolve_picture_url( $template_record['picture_url'] );
727 }
728
729 $booking_form_id = WPBC_BFB_Form_Storage::save_form( $template_record );
730
731 return ! empty( $booking_form_id ) ? (int) $booking_form_id : false;
732 }
733
734
735 /**
736 * Return regular-owner fixtures used by the MultiUser live demo.
737 *
738 * @return array Owner user ID => bundled template key.
739 */
740 function wpbc_bfb_live_demo__get_multiuser_owner_template_map() {
741
742 $owner_templates = array(
743 3 => 'demo_multiuser_owner_1',
744 4 => 'demo_multiuser_owner_2',
745 5 => 'demo_multiuser_owner_3',
746 );
747
748 /**
749 * Allow a private demo installation to use different owner IDs while
750 * retaining the same centralized provisioning flow.
751 *
752 * @param array $owner_templates Owner user ID => template key.
753 */
754 $owner_templates = apply_filters( 'wpbc_bfb_live_demo_multiuser_owner_templates', $owner_templates );
755
756 return is_array( $owner_templates ) ? $owner_templates : array();
757 }
758
759
760 /**
761 * Apply all standard-form fixtures required by the active live demo.
762 *
763 * This is the canonical final activation step. It intentionally replaces demo
764 * fixtures, but never runs on customer installations.
765 *
766 * @return bool True when every applicable fixture was saved.
767 */
768 function wpbc_bfb_live_demo__provision_all_standard_forms() {
769
770 if ( ! function_exists( 'wpbc_is_this_demo' ) || ! wpbc_is_this_demo() ) {
771 return false;
772 }
773
774 // Pro-only reactivation does not necessarily rerun the Free activation
775 // callback that normally synchronizes bundled template rows.
776 if ( function_exists( 'wpbc_bfb_activation__seed_templates' ) ) {
777 wpbc_bfb_activation__seed_templates();
778 }
779
780 $is_success = ( false !== wpbc_bfb_live_demo__provision_standard_form( 0, '', true ) );
781
782 // Form Style is a site-wide setting, not part of an individual form row.
783 // Write directly to the global option so the result is deterministic even
784 // when finalization runs from a regular MultiUser administrator session.
785 update_option( 'booking_form_style', wpbc_bfb_live_demo__get_form_style() );
786
787 if ( class_exists( 'wpdev_bk_multiuser' ) ) {
788 foreach ( wpbc_bfb_live_demo__get_multiuser_owner_template_map() as $owner_user_id => $template_key ) {
789 $owner_user_id = absint( $owner_user_id );
790 $template_key = sanitize_key( (string) $template_key );
791 if ( $owner_user_id <= 0 || '' === $template_key ) {
792 $is_success = false;
793 continue;
794 }
795
796 if ( function_exists( 'get_userdata' ) && ! get_userdata( $owner_user_id ) ) {
797 $is_success = false;
798 continue;
799 }
800
801 if ( false === wpbc_bfb_live_demo__provision_standard_form( $owner_user_id, $template_key, true ) ) {
802 $is_success = false;
803 }
804 }
805 }
806
807 return $is_success;
808 }
809
810
811 /**
812 * Ensure that a regular MultiUser owner has an independent standard BFB form.
813 *
814 * Existing owner forms are never overwritten. On normal installations the
815 * current global standard form is cloned, preserving the Super Admin's chosen
816 * Builder structure. Live demos use their dedicated bundled fixture instead.
817 *
818 * @param int $owner_user_id Regular MultiUser owner ID.
819 *
820 * @return int|false Booking form ID on success, false on failure.
821 */
822 function wpbc_bfb_ensure_standard_form_for_owner( $owner_user_id ) {
823
824 $owner_user_id = absint( $owner_user_id );
825 if ( $owner_user_id <= 0 || ! class_exists( 'WPBC_BFB_Form_Storage' ) ) {
826 return false;
827 }
828 if ( function_exists( 'wpbc_is_table_exists' ) && ! wpbc_is_table_exists( 'booking_form_structures' ) ) {
829 return false;
830 }
831
832 $existing = WPBC_BFB_Form_Storage::get_current_form_by_key( 'standard', $owner_user_id, 'published' );
833 if ( ! empty( $existing->booking_form_id ) ) {
834 return (int) $existing->booking_form_id;
835 }
836
837 if ( function_exists( 'wpbc_is_this_demo' ) && wpbc_is_this_demo() ) {
838 return wpbc_bfb_live_demo__provision_standard_form( $owner_user_id, '', false );
839 }
840
841 $global_standard = WPBC_BFB_Form_Storage::get_current_form_by_key( 'standard', 0, 'published' );
842 $form_record = ! empty( $global_standard ) ? get_object_vars( $global_standard ) : array();
843
844 if ( empty( $form_record['structure_json'] ) && function_exists( 'wpbc_get_bfb_template_record_by_key' ) ) {
845 $form_record = wpbc_get_bfb_template_record_by_key( 'dates_2_columns_sidebar_hints' );
846 }
847 if (
848 empty( $form_record ) ||
849 ! is_array( $form_record ) ||
850 empty( $form_record['structure_json'] ) ||
851 empty( $form_record['engine'] ) ||
852 'bfb' !== (string) $form_record['engine']
853 ) {
854 return false;
855 }
856
857 unset(
858 $form_record['booking_form_id'],
859 $form_record['created_at'],
860 $form_record['created_by'],
861 $form_record['updated_at'],
862 $form_record['updated_by']
863 );
864
865 $form_record['form_slug'] = 'standard';
866 $form_record['status'] = 'published';
867 $form_record['scope'] = 'user';
868 $form_record['owner_user_id'] = $owner_user_id;
869 $form_record['booking_resource_id'] = null;
870 $form_record['is_default'] = 1;
871 $form_record['title'] = __( 'Standard', 'booking' );
872 $form_record['description'] = '';
873
874 $booking_form_id = WPBC_BFB_Form_Storage::save_form( $form_record );
875
876 return ! empty( $booking_form_id ) ? (int) $booking_form_id : false;
877 }
878
879 /**
880 * Import legacy "standard" booking form as initial BFB structure.
881 *
882 * @return void
883 */
884 function wpbc_bfb__import_old_booking_forms() {
885
886 if ( wpbc_bfb_maybe_create_initial_standard_form_from_template() ) {
887 return;
888 }
889
890 if ( function_exists( 'wpbc_bfb_import_legacy_forms' ) ) {
891 wpbc_bfb_import_legacy_forms(
892 array(
893 'mode' => 'all_global',
894 'import_standard' => true,
895 'import_custom' => true,
896 'skip_if_exists' => true,
897 'set_bfb_form_not_defined' => false,
898 )
899 );
900 }
901 }
902
903
904 /**
905 * Remove BFB Preview page from rendered nav menus.
906 *
907 * @param array $sorted_menu_items Menu item objects.
908 * @param stdClass $args Menu args.
909 *
910 * @return array
911 */
912 function wpbc_bfb_preview__exclude_from_nav_menus( $sorted_menu_items, $args ) {
913
914 $page_id = absint( get_option( wpbc_bfb_activation__get_preview_page_option_key() ) );
915
916 if ( $page_id <= 0 ) {
917 return $sorted_menu_items;
918 }
919
920 $filtered_items = array();
921
922 foreach ( $sorted_menu_items as $menu_item ) {
923
924 $object_id = isset( $menu_item->object_id ) ? absint( $menu_item->object_id ) : 0;
925 $object = isset( $menu_item->object ) ? (string) $menu_item->object : '';
926
927 if ( ( 'page' === $object ) && ( $page_id === $object_id ) ) {
928 continue;
929 }
930
931 $filtered_items[] = $menu_item;
932 }
933
934 return $filtered_items;
935 }
936 add_filter( 'wp_nav_menu_objects', 'wpbc_bfb_preview__exclude_from_nav_menus', 10, 2 );
937
938
939 /**
940 * Exclude BFB Preview page from wp_list_pages() outputs.
941 *
942 * @param array $exclude_array Excluded page IDs.
943 *
944 * @return array
945 */
946 function wpbc_bfb_preview__exclude_from_page_lists( $exclude_array ) {
947
948 $page_id = absint( get_option( wpbc_bfb_activation__get_preview_page_option_key() ) );
949
950 if ( $page_id > 0 ) {
951 $exclude_array[] = $page_id;
952 }
953
954 return array_unique( array_map( 'absint', $exclude_array ) );
955 }
956 add_filter( 'wp_list_pages_excludes', 'wpbc_bfb_preview__exclude_from_page_lists' );
957
958
959 /**
960 * Remove BFB Preview page from get_pages() results.
961 *
962 * This is important for block themes, because the core Page List block
963 * renders its items from get_pages().
964 *
965 * @param array $pages Array of page objects.
966 * @param array $parsed_args Parsed get_pages() args.
967 *
968 * @return array
969 */
970 function wpbc_bfb_preview__exclude_from_get_pages( $pages, $parsed_args ) {
971
972 $page_id = absint( get_option( wpbc_bfb_activation__get_preview_page_option_key() ) );
973
974 if ( $page_id <= 0 ) {
975 return $pages;
976 }
977
978 $filtered_pages = array();
979
980 foreach ( $pages as $page_obj ) {
981
982 if ( empty( $page_obj ) || empty( $page_obj->ID ) ) {
983 $filtered_pages[] = $page_obj;
984 continue;
985 }
986
987 if ( absint( $page_obj->ID ) === $page_id ) {
988 continue;
989 }
990
991 $filtered_pages[] = $page_obj;
992 }
993
994 return $filtered_pages;
995 }
996 add_filter( 'get_pages', 'wpbc_bfb_preview__exclude_from_get_pages', 10, 2 );
997
998
999 /**
1000 * Remove BFB Preview page from rendered Navigation Link blocks.
1001 *
1002 * This handles block-theme menus where the Preview page was manually
1003 * inserted as a Navigation Link item.
1004 *
1005 * @param string $block_content Rendered block HTML.
1006 * @param array $block Parsed block data.
1007 * @param WP_Block $instance Block instance.
1008 *
1009 * @return string
1010 */
1011 function wpbc_bfb_preview__exclude_from_navigation_link_block( $block_content, $block, $instance ) {
1012
1013 $page_id = absint( get_option( wpbc_bfb_activation__get_preview_page_option_key() ) );
1014
1015 if ( $page_id <= 0 ) {
1016 return $block_content;
1017 }
1018
1019 if ( empty( $block['attrs'] ) || ! is_array( $block['attrs'] ) ) {
1020 return $block_content;
1021 }
1022
1023 $attrs = $block['attrs'];
1024 $preview_url = get_permalink( $page_id );
1025
1026 $block_id = isset( $attrs['id'] ) ? absint( $attrs['id'] ) : 0;
1027 $block_type = isset( $attrs['type'] ) ? (string) $attrs['type'] : '';
1028 $block_kind = isset( $attrs['kind'] ) ? (string) $attrs['kind'] : '';
1029 $block_url = isset( $attrs['url'] ) ? (string) $attrs['url'] : '';
1030
1031 if ( $block_id === $page_id ) {
1032 return '';
1033 }
1034
1035 if (
1036 ( 'page' === $block_type )
1037 &&
1038 ( ( 'post-type' === $block_kind ) || ( '' === $block_kind ) )
1039 &&
1040 ( ! empty( $block_id ) )
1041 &&
1042 ( $block_id === $page_id )
1043 ) {
1044 return '';
1045 }
1046
1047 if ( ( ! empty( $preview_url ) ) && ( ! empty( $block_url ) ) ) {
1048
1049 $normalized_preview_url = untrailingslashit( $preview_url );
1050 $normalized_block_url = untrailingslashit( $block_url );
1051
1052 if ( $normalized_preview_url === $normalized_block_url ) {
1053 return '';
1054 }
1055 }
1056
1057 return $block_content;
1058 }
1059 add_filter( 'render_block_core/navigation-link', 'wpbc_bfb_preview__exclude_from_navigation_link_block', 10, 3 );
1060
1061
1062 /**
1063 * Remove BFB Preview page from rendered Page List Item blocks.
1064 *
1065 * This is important for block themes like Twenty Twenty-Four,
1066 * because the Navigation block can use a Page List block that renders
1067 * individual core/page-list-item blocks.
1068 *
1069 * @param string $block_content Rendered block HTML.
1070 * @param array $block Parsed block data.
1071 * @param WP_Block $instance Block instance.
1072 *
1073 * @return string
1074 */
1075 function wpbc_bfb_preview__exclude_from_page_list_item_block( $block_content, $block, $instance ) {
1076
1077 $page_id = absint( get_option( wpbc_bfb_activation__get_preview_page_option_key() ) );
1078
1079 if ( $page_id <= 0 ) {
1080 return $block_content;
1081 }
1082
1083 if ( empty( $block['attrs'] ) || ! is_array( $block['attrs'] ) ) {
1084 return $block_content;
1085 }
1086
1087 $attrs = $block['attrs'];
1088 $preview_url = get_permalink( $page_id );
1089
1090 $item_id = isset( $attrs['id'] ) ? absint( $attrs['id'] ) : 0;
1091 $item_link = isset( $attrs['link'] ) ? (string) $attrs['link'] : '';
1092
1093 if ( $item_id === $page_id ) {
1094 return '';
1095 }
1096
1097 if ( ( ! empty( $preview_url ) ) && ( ! empty( $item_link ) ) ) {
1098 if ( untrailingslashit( $preview_url ) === untrailingslashit( $item_link ) ) {
1099 return '';
1100 }
1101 }
1102
1103 return $block_content;
1104 }
1105 add_filter( 'render_block_core/page-list-item', 'wpbc_bfb_preview__exclude_from_page_list_item_block', 10, 3 );
1106
1107
1108 // FixIn: 10.15.3.1.
1109 /**
1110 * Check whether current request is the BFB Preview page request.
1111 *
1112 * @return bool
1113 */
1114 function wpbc_bfb_is_preview_request() {
1115
1116 if ( empty( $_GET['wpbc_bfb_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1117 return false;
1118 }
1119
1120 return ( '1' === sanitize_text_field( wp_unslash( $_GET['wpbc_bfb_preview'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1121 }
1122
1123 /**
1124 * Add noindex / noarchive robots directives for BFB Preview.
1125 *
1126 * @param array $robots Robots directives.
1127 *
1128 * @return array
1129 */
1130 function wpbc_bfb_preview__wp_robots( $robots ) {
1131
1132 if ( ! wpbc_bfb_is_preview_request() ) {
1133 return $robots;
1134 }
1135
1136 return wp_robots_sensitive_page( $robots );
1137 }
1138 add_filter( 'wp_robots', 'wpbc_bfb_preview__wp_robots' );
1139
1140 /**
1141 * Send no-cache headers for BFB Preview responses.
1142 *
1143 * @return void
1144 */
1145 function wpbc_bfb_preview__nocache_headers() {
1146
1147 if ( ! wpbc_bfb_is_preview_request() ) {
1148 return;
1149 }
1150
1151 if ( ! headers_sent() ) {
1152 nocache_headers();
1153 }
1154 }
1155 add_action( 'template_redirect', 'wpbc_bfb_preview__nocache_headers', 1 );
1156
1157 /**
1158 * Import booking forms on activation of Pro versions of Booking Calendar.
1159 *
1160 * $mode = 'current_context'; // Import booking forms only for the == Current User == (usually - "Regular User").
1161 * $mode = 'all_global'; // Import booking forms for == Super Booking Admin == user ( user_id = 0 ). or for pro versions in all other situations.
1162 * $mode = 'all_users'; // Import booking forms for == All Users ==.
1163 *
1164 * @return void
1165 */
1166 function wpbc_bfb_import_legacy_forms__on_activation() {
1167
1168 if ( ! is_admin() ) {
1169 return;
1170 }
1171
1172 if ( false === ( $is_in_action_late_activation = get_transient( 'wpbc_pro_versions__late_activation_60' ) ) ) {
1173 return;
1174 }
1175
1176 delete_transient( 'wpbc_pro_versions__late_activation_60' );
1177
1178 $import_version = '11.3.1';
1179
1180 // Live demos use dedicated BFB fixtures as their canonical source. Importing
1181 // legacy options here would overwrite the fixtures installed during the
1182 // activation request, especially because the old demo importer used
1183 // skip_if_exists=false.
1184 if ( wpbc_is_this_demo() ) {
1185 if ( wpbc_bfb_live_demo__provision_all_standard_forms() ) {
1186 update_option( 'wpbc_bfb_legacy_import_done_for_pro', $import_version );
1187 } else {
1188 // Keep a bounded retry available if activation dependencies were not
1189 // ready yet on this request.
1190 $retry_count = is_numeric( $is_in_action_late_activation ) ? absint( $is_in_action_late_activation ) : 0;
1191 if ( $retry_count < 3 ) {
1192 set_transient( 'wpbc_pro_versions__late_activation_60', $retry_count + 1, HOUR_IN_SECONDS );
1193 }
1194 }
1195 return;
1196 }
1197
1198 if ( $import_version === get_option( 'wpbc_bfb_legacy_import_done_for_pro', '' ) ) {
1199 return;
1200 }
1201
1202 $summary = wpbc_bfb_import_legacy_forms(
1203 array(
1204 'mode' => ( class_exists( 'wpdev_bk_multiuser' ) ) ? 'all_users' : 'all_global',
1205 'import_standard' => true,
1206 'import_custom' => true,
1207 'skip_if_exists' => true,
1208 'set_bfb_form_not_defined' => true,
1209 )
1210 );
1211 update_option( 'wpbc_bfb_legacy_import_done_for_pro', $import_version );
1212 }
1213 add_action( 'init', 'wpbc_bfb_import_legacy_forms__on_activation', 1000 );
1214
1215
1216 /**
1217 * Schedule one deferred legacy-form import after Pro activation. The import runs on a later request while this transient exists.
1218 *
1219 * @return void
1220 */
1221 function wpbc_pro_versions__late_activation(){
1222
1223 // A regular MultiUser owner activation runs the Pro activation callbacks in
1224 // its own context. It must not schedule a later demo-wide reset.
1225 if ( false !== apply_bk_filter( 'wpbc_is_user_in_activation_process' ) ) {
1226 return;
1227 }
1228
1229 // Get any existing copy of our transient data
1230 if ( false === ( $is_in_action_late_activation = get_transient( 'wpbc_pro_versions__late_activation_60' ) ) ) {
1231 // It wasn't there, so regenerate the data and save the transient
1232 $is_in_action_late_activation = true;
1233 set_transient( 'wpbc_pro_versions__late_activation_60', $is_in_action_late_activation, HOUR_IN_SECONDS );
1234 }
1235 }
1236 add_bk_action( 'wpbc_other_versions_activation', 'wpbc_pro_versions__late_activation' );
1237