PluginProbe
Booking Calendar / 11.4
Booking Calendar v11.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 10.11.2 All 203 releases
booking / includes / page-form-builder / ajax / bfb-ajax.php

bfb-ajax.php in Booking Calendar 11.4, at includes/page-form-builder/ajax/bfb-ajax.php

1,851 lines 59.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AJAX controller for Booking Form Builder (BFB) FormConfig.
4 *
5 * Responsibilities:
6 * - Save Builder structure (+ exported shortcodes) into booking_form_structures table.
7 * - Load FormConfig (DB first, legacy options fallback) for the Builder UI.
8 *
9 * This file exposes AJAX endpoints:
10 * - WPBC_AJX_BFB_SAVE_FORM_CONFIG -> wpbc_bfb_ajax_save_form_config()
11 * - WPBC_AJX_BFB_LOAD_FORM_CONFIG -> wpbc_bfb_ajax_load_form_config()
12 * - WPBC_AJX_BFB_CREATE_FORM_CONFIG -> wpbc_bfb_ajax_create_form_config()
13 * - WPBC_AJX_BFB_LIST_FORMS -> wpbc_bfb_ajax_list_forms()
14 * - WPBC_AJX_BFB_DELETE_FORM_CONFIG -> wpbc_bfb_ajax_delete_form_config()
15 * - WPBC_AJX_BFB_DELETE_TEMPLATE_CONFIG -> wpbc_bfb_ajax_delete_template_config()
16 *
17 * Both endpoints work with the normalized FormConfig structure defined in
18 * bfb-form-manager.php.
19 *
20 * @package Booking Calendar.
21 * @subpackage Form Builder
22 *
23 * @since 11.0.0
24 * @file ../includes/page-form-builder/ajax/bfb-ajax.php
25 */
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit;
29 }
30
31 /**
32 * OR separator for template search queries (UI + AJAX).
33 *
34 * Used by wpbc_bfb_ajax_list_forms() to support multi-keyword searches:
35 * "time|duration|slots"
36 *
37 * NOTE:
38 * - This separator is for AJAX search (POST) and UI input.
39 * - Do NOT use it in URLs. Some server configs can block "|" in URLs.
40 * - For URLs, use WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR_URL (default "^").
41 *
42 * @since 11.0.0
43 */
44 if ( ! defined( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR' ) ) {
45 define( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR', '|' );
46 }
47
48 /**
49 * OR separator for template search queries in URLs only.
50 *
51 * Used for redirects like:
52 * &auto_open_template=service^duration
53 *
54 * @since 11.0.0
55 */
56 if ( ! defined( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR_URL' ) ) {
57 define( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR_URL', '^' );
58 }
59
60 // == Helpers == =======================================================================================================
61
62 /**
63 * Get capability required to manage booking forms in the Builder.
64 *
65 * Resolves to a WordPress capability based on the plugin setting
66 * booking_user_role_settings. This keeps Form Builder access aligned with the
67 * rest of the Booking Calendar admin UI.
68 *
69 * Mapping example:
70 * - administrator -> activate_plugins
71 * - editor -> publish_pages
72 * - author -> publish_posts
73 * - contributor -> edit_posts
74 * - subscriber -> read
75 *
76 * If the configured role is not recognized, falls back to manage_options.
77 *
78 * @since 11.0.0
79 *
80 * @return string Capability name.
81 */
82 function wpbc_bfb_get_manage_cap() {
83
84 $min_user_role = get_bk_option( 'booking_user_role_settings' );
85
86 $capability = array(
87 'administrator' => 'activate_plugins',
88 'editor' => 'publish_pages',
89 'author' => 'publish_posts',
90 'contributor' => 'edit_posts',
91 'subscriber' => 'read',
92 );
93
94 if ( isset( $capability[ $min_user_role ] ) ) {
95 return $capability[ $min_user_role ];
96 }
97
98 // Fallback: admins only.
99 return 'manage_options';
100 }
101
102 /**
103 * Extend the list of safe inline CSS properties for BFB-generated markup.
104 *
105 * Callback for the safe_style_css filter. It ensures that BFB-specific inline
106 * styles (including CSS custom properties used by the layout engine) pass
107 * through wp_kses() sanitization.
108 *
109 * The base list of allowed properties is provided by core; this function
110 * appends additional properties if they are not already present.
111 *
112 * You can modify the final list via the wpbc_bfb_safe_style_props filter.
113 *
114 * @since 11.0.0
115 *
116 * @param string[] $styles Array of allowed CSS properties from core.
117 *
118 * @return string[] Modified array including BFB-specific properties.
119 */
120 function wpbc_bfb_safe_style_props_filter( $styles ) {
121
122 $extra_css_props = array(
123 'display',
124 'clear', // used in wizard hidden_style (optional, but safe)
125 'flex-basis', // IMPORTANT: exported per-column layout width
126 // Optional but often useful if you ever output them:
127 'flex',
128 'flex-grow',
129 'flex-shrink',
130 'width',
131 'min-width',
132 'max-width',
133 'box-sizing',
134
135 'transform',
136 'align-self',
137 '--wpbc-bfb-col-dir',
138 '--wpbc-bfb-col-wrap',
139 '--wpbc-bfb-col-jc',
140 '--wpbc-bfb-col-ai',
141 '--wpbc-bfb-col-gap',
142 '--wpbc-bfb-col-ac',
143 '--wpbc-bfb-col-aself',
144 '--wpbc-bfb-form-background',
145 '--wpbc-bfb-form-border-color',
146 '--wpbc-bfb-form-border-width',
147 '--wpbc-bfb-form-border-radius',
148 '--wpbc-bfb-form-padding',
149 '--wpbc-bfb-form-box-shadow',
150 '--wpbc-col-min',
151 );
152
153 /**
154 * Filter extra safe CSS properties for BFB inline styles.
155 *
156 * @since 11.0.0
157 *
158 * @param string[] $extra_css_props List of extra CSS properties.
159 */
160 $extra_css_props = apply_filters( 'wpbc_bfb_safe_style_props', $extra_css_props );
161
162 foreach ( $extra_css_props as $prop ) {
163 if ( ! in_array( $prop, $styles, true ) ) {
164 $styles[] = $prop;
165 }
166 }
167
168 return $styles;
169 }
170
171 /**
172 * Allow STRICT ONLY: transform: translate(... , ...) with numeric/% values
173 *
174 * @param $allow
175 * @param $css_test_string
176 *
177 * @return bool|mixed
178 */
179 function wpbc_bfb_allow_transform_translate_only( $allow, $css_test_string ) {
180 if ( $allow ) {
181 return $allow;
182 }
183
184 $css_test_string = trim( (string) $css_test_string );
185
186 // Allow ONLY: transform: translate(... , ...) with numeric/% values. Also allow px (common for translate), still STRICT.
187 if ( preg_match( '/^transform\s*:\s*translate(?:3d|x|y)?\(\s*-?\d+(?:\.\d+)?(?:%|px)?\s*,\s*-?\d+(?:\.\d+)?(?:%|px)?\s*(?:,\s*-?\d+(?:\.\d+)?(?:%|px)?\s*)?\)\s*$/i', $css_test_string ) ) {
188 return true;
189 }
190
191 return false;
192 }
193
194 /**
195 * Sanitize advanced/content booking form text coming from the Builder.
196 *
197 * @since 11.0.0
198 *
199 * @param string $form_value Raw form markup (may be slashed).
200 *
201 * @return string Sanitized form markup.
202 */
203 function wpbc_bfb_sanitize_form_text( $form_value ) {
204
205 $form_value = (string) $form_value;
206
207 if ( '' === $form_value ) {
208 return '';
209 }
210
211 // Make function self-contained for all call-sites.
212 $form_value = wp_unslash( $form_value );
213 $form_value = wp_kses_no_null( $form_value );
214
215 // Optional but recommended: avoid comment encoding artifacts.
216 // Remove this if you must preserve comments in DB exactly as-is.
217 $form_value = preg_replace( '/<!--[\s\S]*?-->/', '', $form_value );
218
219 // Start with WP default allowed tags, then extend with our custom tags (custom wins).
220 $allowed_tags = array_merge(
221 wp_kses_allowed_html( 'post' ),
222 wpbc_get_allowed_simple_html_tags__for_wp_kses() // Custom short tags used in legacy / advanced markup. // FixIn: 10.15.5.6.
223 );
224
225 // Allow 'name' on <p> (if used by legacy markup).
226 if ( isset( $allowed_tags['p'] ) ) {
227 $allowed_tags['p']['name'] = true;
228 }
229
230 // Extra attributes for layout/structure wrappers.
231 foreach ( array( 'div', 'span', 'hr' ) as $tag ) {
232 if ( ! isset( $allowed_tags[ $tag ] ) ) {
233 $allowed_tags[ $tag ] = array();
234 }
235 $allowed_tags[ $tag ]['data-bfb-type'] = true;
236 $allowed_tags[ $tag ]['data-orientation'] = true;
237 $allowed_tags[ $tag ]['name'] = true;
238 $allowed_tags[ $tag ]['aria-orientation'] = true;
239 }
240
241 // Temporarily allow extra inline style properties for BFB.
242 add_filter( 'safe_style_css', 'wpbc_bfb_safe_style_props_filter', 10, 1 );
243
244 // Allow ONLY transform: translate*(...) patterns (your strict validator).
245 add_filter( 'safecss_filter_attr_allow_css', 'wpbc_bfb_allow_transform_translate_only', 10, 2 );
246
247 $sanitized = wp_kses( $form_value, $allowed_tags );
248
249 remove_filter( 'safecss_filter_attr_allow_css', 'wpbc_bfb_allow_transform_translate_only', 10 );
250 remove_filter( 'safe_style_css', 'wpbc_bfb_safe_style_props_filter', 10 );
251
252 return $sanitized;
253 }
254
255 /**
256 * Sanitize a form slug/key.
257 *
258 * Allows: a-z, 0-9, underscore, dash.
259 *
260 * @param string $raw
261 *
262 * @return string
263 */
264 function wpbc_bfb__sanitize_form_slug( $raw ) {
265
266 $raw = strtolower( trim( sanitize_text_field( (string) $raw ) ) );
267
268 // Replace spaces with underscore for readability.
269 $raw = preg_replace( '/\s+/', '_', $raw );
270
271 // Keep only: a-z 0-9 _ -
272 $raw = preg_replace( '/[^a-z0-9_\-]/', '_', $raw );
273
274 // Collapse multiple separators.
275 $raw = preg_replace( '/[_\-]{2,}/', '_', $raw );
276
277 // Trim separators.
278 $raw = trim( $raw, '_-' );
279
280 return $raw;
281 }
282
283 /**
284 * Read form_details from POST (array or JSON string) and sanitize values.
285 *
286 * Keys:
287 * - form_name
288 * - title
289 * - description
290 * - picture_url
291 *
292 * IMPORTANT: We keep "presence" checks with array_key_exists() in the caller,
293 * so UI can intentionally clear values by sending empty string.
294 *
295 * @param mixed $raw
296 *
297 * @return array
298 */
299 function wpbc_bfb__normalize_form_details_from_post( $raw ) {
300
301 if ( is_string( $raw ) && '' !== $raw ) {
302 $tmp = json_decode( $raw, true );
303 if ( is_array( $tmp ) ) {
304 $raw = $tmp;
305 }
306 }
307
308 if ( ! is_array( $raw ) ) {
309 return array();
310 }
311
312 $out = array();
313
314 if ( array_key_exists( 'form_name', $raw ) ) {
315 $out['form_name'] = sanitize_text_field( $raw['form_name'] );
316 }
317
318 if ( array_key_exists( 'title', $raw ) ) {
319 $out['title'] = sanitize_text_field( (string) $raw['title'] );
320 }
321
322 if ( array_key_exists( 'description', $raw ) ) {
323 $out['description'] = sanitize_textarea_field( (string) $raw['description'] );
324 }
325
326 if ( array_key_exists( 'picture_url', $raw ) ) {
327 $out['picture_url'] = esc_url_raw( (string) $raw['picture_url'] );
328 }
329
330 return $out;
331 }
332
333 /**
334 * Split a search string into OR-terms by configured separator.
335 *
336 * Example (default "~"):
337 * - "time~duration~slots" => array( 'time', 'duration', 'slots' )
338 * - " time ~ duration " => array( 'time', 'duration' )
339 *
340 * @since 11.0.0
341 *
342 * @param string $search_raw Raw search string.
343 * @param int $max_terms Max number of terms allowed (anti-abuse).
344 *
345 * @return array List of unique, trimmed terms.
346 */
347 function wpbc_bfb__split_search_terms_by_or_separator( $search_raw, $max_terms = 5 ) {
348
349 $search_raw = trim( (string) $search_raw );
350
351 if ( '' === $search_raw ) {
352 return array();
353 }
354
355 $max_terms = absint( $max_terms );
356 if ( $max_terms <= 0 ) {
357 $max_terms = 5;
358 }
359
360
361 $sep = ( defined( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR' ) ) ? (string) WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR : '|';
362 if ( '' === $sep ) {
363 $sep = '|';
364 }
365
366 $pattern = '/\s*' . preg_quote( $sep, '/' ) . '\s*/';
367 $parts = preg_split( $pattern, $search_raw );
368
369 if ( ! is_array( $parts ) ) {
370 return array();
371 }
372
373 $terms = array();
374
375 foreach ( $parts as $p ) {
376 $t = trim( (string) $p );
377 if ( '' === $t ) {
378 continue;
379 }
380 $terms[] = $t;
381 if ( count( $terms ) >= $max_terms ) {
382 break;
383 }
384 }
385
386 $terms = array_values( array_unique( $terms ) );
387
388 return $terms;
389 }
390
391 /**
392 * Normalize settings into array() and ensure ONLY supported schema exists:
393 * {
394 * options : {},
395 * css_vars : [],
396 * bfb_options : { advanced_mode_source: 'builder'|'advanced'|'auto' }
397 * }
398 *
399 * @param mixed $settings
400 *
401 * @return array
402 */
403 function wpbc_bfb__normalize_settings_array( $settings ) {
404
405 if ( is_string( $settings ) && '' !== $settings ) {
406 $tmp = json_decode( $settings, true );
407 if ( is_array( $tmp ) ) {
408 $settings = $tmp;
409 }
410 }
411
412 if ( ! is_array( $settings ) ) {
413 $settings = array();
414 }
415
416 if ( empty( $settings['options'] ) || ! is_array( $settings['options'] ) ) {
417 $settings['options'] = array();
418 }
419
420 if ( function_exists( 'wpbc_bfb_settings__strip_form_style_options_from_form_settings' ) ) {
421 $settings = wpbc_bfb_settings__strip_form_style_options_from_form_settings( $settings );
422 }
423
424 if ( empty( $settings['css_vars'] ) || ! is_array( $settings['css_vars'] ) ) {
425 $settings['css_vars'] = array();
426 }
427
428 if ( empty( $settings['bfb_options'] ) || ! is_array( $settings['bfb_options'] ) ) {
429 $settings['bfb_options'] = array();
430 }
431
432 $src = isset( $settings['bfb_options']['advanced_mode_source'] ) ? strtolower( trim( (string) $settings['bfb_options']['advanced_mode_source'] ) ) : 'auto';
433 if ( ! in_array( $src, array( 'builder', 'advanced', 'auto' ), true ) ) {
434 $src = 'auto';
435 }
436 $settings['bfb_options']['advanced_mode_source'] = $src;
437
438 return $settings;
439 }
440
441 /**
442 * Normalize preview-only global Form Style override.
443 *
444 * @param mixed $preview_form_style Raw JSON string or array.
445 * @return array
446 */
447 function wpbc_bfb__normalize_preview_form_style( $preview_form_style ) {
448
449 if ( is_string( $preview_form_style ) && '' !== trim( $preview_form_style ) ) {
450 $decoded = json_decode( $preview_form_style, true );
451 if ( is_array( $decoded ) ) {
452 $preview_form_style = $decoded;
453 }
454 }
455
456 if ( ! is_array( $preview_form_style ) ) {
457 return array();
458 }
459
460 $style = isset( $preview_form_style['booking_form_style'] ) ? $preview_form_style['booking_form_style'] : '';
461 $style = function_exists( 'wpbc_bfb_settings__sanitize_form_style' )
462 ? wpbc_bfb_settings__sanitize_form_style( $style )
463 : sanitize_key( (string) $style );
464
465 $custom_options = function_exists( 'wpbc_bfb_settings__get_custom_form_style_options' )
466 ? wpbc_bfb_settings__get_custom_form_style_options( $preview_form_style )
467 : array();
468
469 return array_merge(
470 array(
471 'booking_form_style' => $style,
472 ),
473 $custom_options
474 );
475 }
476
477 /**
478 * Check whether template key means "blank form".
479 *
480 * @param string $template_form_name
481 *
482 * @return bool
483 */
484 function wpbc_bfb__is_blank_template_key( $template_form_name ) {
485
486 $template_form_name = (string) $template_form_name;
487
488 return ( '' === $template_form_name || '__blank__' === $template_form_name || 'blank' === $template_form_name );
489 }
490
491 /**
492 * Get blank Builder structure seed.
493 *
494 * @return array
495 */
496 function wpbc_bfb__get_blank_structure_seed() {
497
498 return array(
499 array(
500 'page' => 1,
501 'content' => array(),
502 ),
503 );
504 }
505
506 /**
507 * Resolve BFB form/template picture URL.
508 *
509 * Rules:
510 * - If value is already an absolute URL, return as is.
511 * - If value is only a file name, first try local bundled templates image folder:
512 * ../includes/page-form-builder/save-load/../assets/template-img/
513 * - If local file does not exist, use external fallback base URL.
514 *
515 * @param string $picture_url Raw picture_url value from DB.
516 *
517 * @return string
518 */
519 function wpbc_bfb_resolve_picture_url( $picture_url ) {
520
521 $picture_url = trim( (string) $picture_url );
522
523 if ( '' === $picture_url ) {
524 return '';
525 }
526
527 // Already absolute URL or protocol-relative URL.
528 if (
529 ( false !== strpos( $picture_url, '://' ) ) ||
530 ( 0 === strpos( $picture_url, '//' ) )
531 ) {
532 return $picture_url;
533 }
534
535 // If path contains directories, treat it as already prepared relative path.
536 // This helper is intended mainly for simple file names like "template_appointments_01.png".
537 if (
538 ( false !== strpos( $picture_url, '/' ) ) ||
539 ( false !== strpos( $picture_url, '\\' ) )
540 ) {
541 return $picture_url;
542 }
543
544 $file_name = sanitize_file_name( wp_basename( $picture_url ) );
545 if ( '' === $file_name ) {
546 return '';
547 }
548
549 $local_dir_path = trailingslashit( plugin_dir_path( __FILE__ ) ) . '../assets/template-img/';
550 $local_file_path = $local_dir_path . $file_name;
551
552 if ( file_exists( $local_file_path ) ) {
553 return trailingslashit( plugin_dir_url( __FILE__ ) ) . '../assets/template-img/' . rawurlencode( $file_name );
554 }
555
556 $fallback_base_url = apply_filters( 'wpbc_bfb_template_picture_fallback_base_url', 'https://wpbookingcalendar.com/assets/template-img/' );
557
558 return trailingslashit( $fallback_base_url ) . rawurlencode( $file_name );
559 }
560
561 /**
562 * Verify AJAX delete nonce for BFB delete operations.
563 *
564 * Preferred nonce:
565 * - wpbc_bfb_form_delete
566 *
567 * Backward-compatible fallback:
568 * - wpbc_bfb_form_list
569 *
570 * This fallback allows template deletion from the Apply Template modal
571 * even if only nonce_list is localized in older builder pages.
572 *
573 * @since 11.0.0
574 *
575 * @return bool
576 */
577 function wpbc_bfb__verify_delete_request_nonce() {
578
579 if ( check_ajax_referer( 'wpbc_bfb_form_delete', 'nonce', false ) ) {
580 return true;
581 }
582
583 if ( check_ajax_referer( 'wpbc_bfb_form_list', 'nonce', false ) ) {
584 return true;
585 }
586
587 return false;
588 }
589
590 /**
591 * Check whether a listed template can be deleted in the current owner context.
592 *
593 * Rules:
594 * - Only rows with status=template are deletable.
595 * - Reserved/default templates are never deletable.
596 * - In MU regular-user context, only own templates are deletable.
597 * - In global/admin context, only global templates are deletable.
598 *
599 * @since 11.0.0
600 *
601 * @param string $form_slug Template slug.
602 * @param int $row_owner_user_id Owner of listed row.
603 * @param int $current_owner_user_id Current owner context.
604 * @param int $is_default Default flag.
605 * @param string $status Row status.
606 *
607 * @return bool
608 */
609 function wpbc_bfb__can_delete_template_in_current_context( $form_slug, $row_owner_user_id, $current_owner_user_id, $is_default, $status ) {
610
611 if ( 'template' !== (string) $status ) {
612 return false;
613 }
614
615 if ( 'standard' === (string) $form_slug ) {
616 return false;
617 }
618
619 if ( 1 === absint( $is_default ) ) {
620 return false;
621 }
622
623 $row_owner_user_id = absint( $row_owner_user_id );
624 $current_owner_user_id = absint( $current_owner_user_id );
625
626 if ( $current_owner_user_id > 0 ) {
627 return ( $row_owner_user_id === $current_owner_user_id );
628 }
629
630 return ( 0 === $row_owner_user_id );
631 }
632
633 // == AJAX == ==========================================================================================================
634
635
636 /**
637 * Handle AJAX request: save FormConfig from the Form Builder.
638 *
639 * Security:
640 * - Verifies wpbc_bfb_form_save nonce (sent as 'nonce').
641 * - Requires current_user_can( wpbc_bfb_get_manage_cap() ).
642 *
643 * Expects POST:
644 * - nonce : string Nonce for 'wpbc_bfb_form_save'.
645 * - form_name : string 'standard' or custom key (optional, default 'standard').
646 * - engine : string Engine name, usually 'bfb' (optional, default 'bfb').
647 * - engine_version : string Engine version (optional, default '1.0').
648 * - structure : string JSON string (Builder structure).
649 * - settings : string JSON string (extra settings, optional).
650 * - advanced_form : string Shortcodes / markup for booking form (optional).
651 * - content_form : string Shortcodes / markup for "Content of booking fields data" (optional).
652 *
653 * On success:
654 * - Persists FormConfig via wpbc_form_config_save() (which writes to
655 * booking_form_structures and optionally syncs legacy options).
656 *
657 * Response (JSON):
658 * - success: true|false
659 * - data: {
660 * booking_form_id: int,
661 * form_name: string,
662 * engine: string
663 * }
664 *
665 * @since 11.0.0
666 *
667 * @return void
668 */
669 function wpbc_bfb_ajax_save_form_config() {
670 global $wpdb;
671
672 if ( ! check_ajax_referer( 'wpbc_bfb_form_save', 'nonce', false ) ) {
673 wp_send_json_error( array( 'code' => 'invalid_nonce', 'message' => __( 'Security check failed.', 'booking' ) ) );
674 }
675
676 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
677 wp_send_json_error( array( 'code' => 'forbidden', 'message' => __( 'You are not allowed to save booking forms.', 'booking' ) ) );
678 }
679
680 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
681 $form_name = isset( $_POST['form_name'] ) ? wpbc_bfb__sanitize_form_slug( wp_unslash( $_POST['form_name'] ) ) : '';
682 if ( '' === $form_name ) {
683 $form_name = 'standard';
684 }
685
686 $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published';
687 $allowed_statuses = array( 'published', 'preview', 'template' );
688 if ( ! in_array( $status, $allowed_statuses, true ) ) {
689 $status = 'published';
690 }
691
692 // Preview context ID (calendar/resource) used ONLY to build preview URL + render shortcode. It is NOT saved into FormConfig in BFB mode !
693 $preview_form_id = isset( $_POST['preview_form_id'] ) ? absint( wp_unslash( $_POST['preview_form_id'] ) ) : 0;
694 if ( $preview_form_id <= 0 ) {
695 $preview_form_id = 1;
696 }
697 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
698 $return_preview_url = ( isset( $_POST['return_preview_url'] ) && '1' === (string) wp_unslash( $_POST['return_preview_url'] ) );
699
700 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
701 $engine = isset( $_POST['engine'] ) ? sanitize_text_field( wp_unslash( $_POST['engine'] ) ) : 'bfb';
702 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
703 $engine_version = isset( $_POST['engine_version'] ) ? sanitize_text_field( wp_unslash( $_POST['engine_version'] ) ) : '1.0';
704
705 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
706 $structure_raw = isset( $_POST['structure'] ) ? wp_unslash( $_POST['structure'] ) : '';
707 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
708 $settings_raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
709 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
710 $preview_form_style_raw = isset( $_POST['preview_form_style'] ) ? wp_unslash( $_POST['preview_form_style'] ) : '';
711
712 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
713 $content_form_raw = isset( $_POST['content_form'] ) ? wp_unslash( $_POST['content_form'] ) : '';
714 $content_form = wpbc_bfb_sanitize_form_text( $content_form_raw );
715
716 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
717 $advanced_form_raw = isset( $_POST['advanced_form'] ) ? wp_unslash( $_POST['advanced_form'] ) : '';
718 $advanced_form = wpbc_bfb_sanitize_form_text( $advanced_form_raw );
719
720
721 // Validate structure JSON.
722 $structure_arr = json_decode( $structure_raw, true );
723 if ( ! is_array( $structure_arr ) ) {
724 wp_send_json_error( array( 'code' => 'invalid_structure', 'message' => __( 'Form structure is not a valid JSON object.', 'booking' ) ) );
725 }
726
727 // Settings JSON (normalized to the ONLY supported schema).
728 $settings_arr = wpbc_bfb__normalize_settings_array( $settings_raw );
729 $preview_form_style = wpbc_bfb__normalize_preview_form_style( $preview_form_style_raw );
730 // $advanced_mode_source = ( isset( $settings_arr['bfb_options']['advanced_mode_source'] ) ) ? (string) $settings_arr['bfb_options']['advanced_mode_source'] : 'builder';
731
732 // Check if owner of this form is "Regular User" in MU.
733 $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
734
735 $form_config = array(
736 'form_name' => $form_name,
737 'engine' => $engine,
738 'engine_version' => $engine_version,
739 'structure_json' => wpbc_form_config__encode_json( $structure_arr ),
740 'settings' => $settings_arr,
741 'advanced_form' => $advanced_form,
742 'content_form' => $content_form,
743 'owner_user_id' => $owner_user_id,
744 'scope' => ( $owner_user_id > 0 ) ? 'user' : 'global',
745 'status' => $status,
746 'is_default' => ( ( 'standard' === $form_name ) && ( 'template' !== $status ) ) ? 1 : 0,
747 'booking_resource_id' => null,
748 );
749
750 // ---------------------------------------------------------------------
751 // Form Details (title/description/picture) coming from UI.
752 // - Preserve existing values unless UI explicitly sent a key.
753 // ---------------------------------------------------------------------
754
755 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
756 $form_details_raw = isset( $_POST['form_details'] ) ? wp_unslash( $_POST['form_details'] ) : null;
757 $form_details = wpbc_bfb__normalize_form_details_from_post( $form_details_raw );
758
759 $existing_cfg = wpbc_form_config_load( $form_name, $owner_user_id );
760
761 // Optional: rename slug/key (save by booking_form_id to avoid creating a duplicate).
762 if ( array_key_exists( 'form_name', $form_details ) && '' !== $form_details['form_name'] ) {
763
764 $new_form_name = (string) $form_details['form_name'];
765
766 // Block reserved.
767 if ( 'standard' === $new_form_name && 'standard' !== $form_name ) {
768 wp_send_json_error( array( 'code' => 'reserved', 'message' => __( 'This form key is reserved.', 'booking' ) ) );
769 }
770
771 // If slug changed, ensure no collision.
772 if ( $new_form_name !== $form_name ) {
773
774 $is_fallback_to_legacy = false;
775 $collision = wpbc_form_config_load( $new_form_name, $owner_user_id, 'published', $is_fallback_to_legacy );
776
777 $existing_id = ( is_array( $existing_cfg ) && isset( $existing_cfg['id'] ) ) ? absint( $existing_cfg['id'] ) : 0;
778 $collision_id = ( is_array( $collision ) && isset( $collision['id'] ) ) ? absint( $collision['id'] ) : 0;
779
780 if ( ! empty( $collision ) && $collision_id !== $existing_id ) {
781 wp_send_json_error( array( 'code' => 'already_exists', 'message' => __( 'Form key already exists. Please choose another.', 'booking' ) ) );
782 }
783
784 // Save by ID (so wpbc_form_config_save updates this row).
785 if ( $existing_id > 0 ) {
786 $form_config['booking_form_id'] = $existing_id;
787 }
788
789 $form_name = $new_form_name;
790
791 // Keep flags consistent.
792 $form_config['form_name'] = $form_name;
793 $form_config['is_default'] = ( 'standard' === $form_name ) ? 1 : 0;
794 }
795 }
796
797
798 $existing_title = ( is_array( $existing_cfg ) && isset( $existing_cfg['title'] ) ) ? (string) $existing_cfg['title'] : '';
799 $existing_desc = ( is_array( $existing_cfg ) && isset( $existing_cfg['description'] ) ) ? (string) $existing_cfg['description'] : '';
800 $existing_pic = ( is_array( $existing_cfg ) && isset( $existing_cfg['picture_url'] ) ) ? (string) $existing_cfg['picture_url'] : '';
801
802 // Default: keep existing if set, otherwise fallback.
803 $form_title = ( '' !== trim( $existing_title ) ) ? $existing_title : ( ( 'standard' === $form_name ) ? __( 'Standard', 'booking' ) : $form_name );
804 $form_desc = $existing_desc;
805 $form_pic = $existing_pic;
806
807 // 1) Preferred: override from form_details if key exists (supports clearing).
808 if ( array_key_exists( 'title', $form_details ) ) {
809 $form_title = (string) $form_details['title'];
810 }
811
812 if ( array_key_exists( 'description', $form_details ) ) {
813 $form_desc = (string) $form_details['description'];
814 }
815
816 if ( array_key_exists( 'picture_url', $form_details ) ) {
817 $form_pic = (string) $form_details['picture_url'];
818 }
819
820 // 2) Backward compatibility: keep your old options override (if still used elsewhere).
821 if ( ! empty( $settings_arr['options'] ) && is_array( $settings_arr['options'] ) ) {
822
823 $options = $settings_arr['options'];
824
825 if ( array_key_exists( 'booking_form_title', $options ) && ! array_key_exists( 'title', $form_details ) ) {
826 $form_title = sanitize_text_field( $options['booking_form_title'] );
827 }
828
829 if ( array_key_exists( 'booking_form_description', $options ) && ! array_key_exists( 'description', $form_details ) ) {
830 $form_desc = sanitize_textarea_field( $options['booking_form_description'] );
831 }
832 }
833
834 // Store final meta into columns.
835 $form_config['title'] = $form_title;
836 $form_config['description'] = $form_desc;
837 $form_config['picture_url'] = $form_pic;
838
839 // Apply (possibly adjusted) settings back into form_config (important).
840 $form_config['settings'] = wpbc_bfb__normalize_settings_array( $settings_arr );
841
842 // We do not need to update options: 'booking_form', etc... in BFB!
843 $sync_legacy = false;
844 // == One Saving point ==
845 $booking_form_id = wpbc_form_config_save( $form_config, array( 'sync_legacy' => (bool) $sync_legacy ) );
846
847 if ( ! $booking_form_id ) {
848
849 wp_send_json_error(
850 array(
851 'code' => 'save_failed',
852 'message' => __( 'Error saving booking form.', 'booking' ) . ( ! empty( $wpdb->last_error ) ? ' ' . $wpdb->last_error : '' ),
853 )
854 );
855 }
856
857
858 $preview_url = '';
859 $preview_token = '';
860
861 if ( $return_preview_url && 'preview' === $status && class_exists( 'WPBC_BFB_Preview_Service' ) ) {
862
863 $preview_service = WPBC_BFB_Preview_Service::get_instance();
864
865 $res = $preview_service->create_preview_session( $preview_form_id, wpbc_get_current_user_id(), $structure_arr, $form_name, $advanced_form, $content_form, $preview_form_style );
866
867 if ( is_array( $res ) && ! empty( $res['preview_url'] ) ) {
868 $preview_url = (string) $res['preview_url'];
869 $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : '';
870 }
871 }
872
873 $setup_step_saved = false;
874 $setup_step = isset( $_POST['wpbc_setup_step'] ) ? sanitize_key( wp_unslash( $_POST['wpbc_setup_step'] ) ) : '';
875 if ( ! empty( $setup_step ) && class_exists( 'WPBC_SETUP_WIZARD_STEPS' ) ) {
876 $setup_steps = new WPBC_SETUP_WIZARD_STEPS();
877 $steps_arr = $setup_steps->get_steps_arr();
878 if ( function_exists( 'wpbc_setup_wizard__detect_step_from_admin_url' ) ) {
879 $referer_step = wpbc_setup_wizard__detect_step_from_admin_url( wp_get_referer() );
880 if ( ! empty( $referer_step ) && isset( $steps_arr[ $referer_step ] ) ) {
881 $setup_step = $referer_step;
882 }
883 }
884 if ( isset( $steps_arr[ $setup_step ] ) ) {
885 $setup_steps->db__set_step_as_saved( $setup_step, true );
886 $setup_steps->db__save_current_step_name( $setup_step );
887 $setup_step_saved = true;
888 }
889 }
890
891 wp_send_json_success(
892 array(
893 'booking_form_id' => $booking_form_id,
894 'form_name' => $form_name,
895 'engine' => $engine,
896 'status' => $status,
897 'preview_url' => $preview_url,
898 'token' => $preview_token,
899 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : '',
900 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '',
901 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : '',
902 'setup_step_saved' => $setup_step_saved,
903 )
904 );
905
906 }
907 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_SAVE_FORM_CONFIG', 'wpbc_bfb_ajax_save_form_config' );
908
909
910 /**
911 * Handle AJAX request: save FormConfig as TEMPLATE.
912 *
913 * This is a minimal wrapper around wpbc_bfb_ajax_save_form_config().
914 * It forces status='template' and reuses all validations/sanitizers.
915 *
916 * @since 11.0.0
917 *
918 * @return void
919 */
920 function wpbc_bfb_ajax_save_form_config_template() {
921
922 // Force template status (listing expects status='template').
923 $_POST['status'] = 'template';
924
925 // Reuse main save logic.
926 wpbc_bfb_ajax_save_form_config();
927 }
928 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_SAVE_FORM_CONFIG_TEMPLATE', 'wpbc_bfb_ajax_save_form_config_template' );
929
930
931 /**
932 * Handle AJAX request: load FormConfig for the Form Builder.
933 *
934 * Security:
935 * - Verifies wpbc_bfb_form_load nonce (sent as 'nonce').
936 * - Requires current_user_can( wpbc_bfb_get_manage_cap() ).
937 *
938 * Expects POST:
939 * - nonce : string Nonce for 'wpbc_bfb_form_load'.
940 * - form_name : string 'standard' or custom key (optional, default 'standard').
941 *
942 * Behaviour:
943 * - Loads FormConfig via wpbc_form_config_load().
944 * - For engine = 'bfb', decodes structure_json into 'structure' array.
945 * - For engine = 'legacy_*', returns a simple "notice" structure in Builder canvas.
946 *
947 * @since 11.0.0
948 *
949 * @return void
950 */
951 function wpbc_bfb_ajax_load_form_config() {
952
953 if ( ! check_ajax_referer( 'wpbc_bfb_form_load', 'nonce', false ) ) {
954 wp_send_json_error(
955 array(
956 'code' => 'invalid_nonce',
957 'message' => __( 'Security check failed.', 'booking' ),
958 )
959 );
960 }
961
962 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
963 wp_send_json_error(
964 array(
965 'code' => 'forbidden',
966 'message' => __( 'You are not allowed to load booking forms.', 'booking' ),
967 )
968 );
969 }
970
971 $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
972 if ( '' === $form_name ) {
973 $form_name = 'standard';
974 }
975
976 $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published';
977 $allowed_statuses = array( 'published', 'preview', 'template' );
978 if ( ! in_array( $status, $allowed_statuses, true ) ) {
979 $status = 'published';
980 }
981
982
983 // Check if owner of this form is "Regular User" in MU.
984 $user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
985
986 if ( ! empty( $user_id ) ) {
987 make_bk_action( 'check_multiuser_params_for_client_side_by_user_id', $user_id ); // == MU == // FixIn: 2026-03-06 11:57.
988 }
989
990 $form_config = wpbc_form_config_load( $form_name, $user_id, $status );
991
992 if ( ! empty( $user_id ) ) {
993 make_bk_action( 'finish_check_multiuser_params_for_client_side', null ); // == MU == // FixIn: 2026-03-06 11:57.
994 }
995
996 if ( empty( $form_config ) || ( ! is_array( $form_config ) ) ) {
997 wp_send_json_error(
998 array(
999 'code' => 'not_found',
1000 'message' => __( 'Booking form configuration not found.', 'booking' ),
1001 ),
1002 404
1003 );
1004 }
1005
1006 $engine = isset( $form_config['engine'] ) ? (string) $form_config['engine'] : '';
1007 $structure = array();
1008
1009 if ( ! empty( $form_config['structure_json'] ) ) {
1010 $tmp = json_decode( $form_config['structure_json'], true );
1011 if ( is_array( $tmp ) ) {
1012 $structure = $tmp;
1013 }
1014 }
1015
1016 // Advanced Mode notice only when no visual Builder structure exists.
1017 if ( empty( $structure ) && 'advanced_mode' === $engine ) {
1018
1019 $structure = array(
1020 array(
1021 'page' => 1,
1022 'content' => array(
1023 array(
1024 'type' => 'field',
1025 'data' => array(
1026 'id' => 'static_text_legacy_notice_1',
1027 'type' => 'static_text',
1028 'usage_key' => 'static_text',
1029 'text' => __( 'This imported form is currently configured in Advanced Form mode only.', 'booking' ),
1030 'tag' => 'p',
1031 'align' => 'center',
1032 'bold' => 1,
1033 'italic' => 0,
1034 'html_allowed' => 0,
1035 'nl2br' => 1,
1036 'name' => 'static_text_legacy_notice_1',
1037 'html_id' => '',
1038 'cssclass_extra' => '',
1039 'label' => 'Static_text',
1040 ),
1041 ),
1042 array(
1043 'type' => 'field',
1044 'data' => array(
1045 'id' => 'static_text_legacy_notice_2',
1046 'type' => 'static_text',
1047 'usage_key' => 'static_text',
1048 'text' => __( 'Nothing is broken - the form was imported and can be edited in Advanced Mode. You can also start building visually by dragging fields from Add Fields onto this canvas.', 'booking' ),
1049 'tag' => 'p',
1050 'align' => 'center',
1051 'bold' => 0,
1052 'italic' => 0,
1053 'html_allowed' => 0,
1054 'nl2br' => 1,
1055 'name' => 'static_text_legacy_notice_2',
1056 'html_id' => '',
1057 'cssclass_extra' => '',
1058 'label' => 'Static_text',
1059 ),
1060 ),
1061 ),
1062 ),
1063 );
1064 }
1065
1066 $settings_out = wpbc_bfb__normalize_settings_array( isset( $form_config['settings'] ) ? $form_config['settings'] : array() );
1067
1068 wp_send_json_success(
1069 array(
1070 'form_name' => isset( $form_config['form_name'] ) ? (string) $form_config['form_name'] : $form_name,
1071 'engine' => $engine,
1072 'engine_version' => isset( $form_config['engine_version'] ) ? (string) $form_config['engine_version'] : '',
1073 'structure' => $structure,
1074 'settings' => $settings_out,
1075 'advanced_form' => isset( $form_config['advanced_form'] ) ? (string) $form_config['advanced_form'] : '',
1076 'content_form' => isset( $form_config['content_form'] ) ? (string) $form_config['content_form'] : '',
1077 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : '',
1078 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '',
1079 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : '',
1080 )
1081 );
1082 }
1083 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_LOAD_FORM_CONFIG', 'wpbc_bfb_ajax_load_form_config' );
1084
1085
1086 /**
1087 * Handle AJAX request: create new FormConfig by cloning a template form,
1088 * or creating a blank form when template is not selected / not available.
1089 *
1090 * Expects POST:
1091 * - nonce
1092 * - form_name (new form key / slug)
1093 * - template_form_name (optional; '' or '__blank__' => blank form)
1094 * - title (optional)
1095 * - description (optional)
1096 * - image_url (optional)
1097 */
1098 function wpbc_bfb_ajax_create_form_config() {
1099
1100 if ( ! check_ajax_referer( 'wpbc_bfb_form_create', 'nonce', false ) ) {
1101 wp_send_json_error(
1102 array(
1103 'code' => 'invalid_nonce',
1104 'message' => __( 'Security check failed.', 'booking' ),
1105 )
1106 );
1107 }
1108
1109 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
1110 wp_send_json_error(
1111 array(
1112 'code' => 'forbidden',
1113 'message' => __( 'You are not allowed to create booking forms.', 'booking' ),
1114 )
1115 );
1116 }
1117
1118 // New form key.
1119 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1120 $form_name = isset( $_POST['form_name'] ) ? wpbc_bfb__sanitize_form_slug( wp_unslash( $_POST['form_name'] ) ) : '';
1121 if ( '' === $form_name ) {
1122 wp_send_json_error(
1123 array(
1124 'code' => 'invalid_form_name',
1125 'message' => __( 'Form key is required.', 'booking' ),
1126 )
1127 );
1128 }
1129 if ( 'standard' === $form_name ) {
1130 wp_send_json_error( array( 'code' => 'reserved', 'message' => __( 'This form key is reserved.', 'booking' ) ) );
1131 }
1132
1133 // Template key (optional).
1134 $template_form_name = isset( $_POST['template_form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['template_form_name'] ) ) : '';
1135
1136 $is_blank = wpbc_bfb__is_blank_template_key( $template_form_name );
1137
1138 // Meta.
1139 $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';
1140 $description = isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '';
1141 $image_url = isset( $_POST['image_url'] ) ? esc_url_raw( wp_unslash( $_POST['image_url'] ) ) : '';
1142
1143 if ( '' === $title ) {
1144 $title = $form_name;
1145 }
1146
1147 // MU owner logic.
1148 $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
1149
1150 // Ensure new form does not already exist.
1151 $is_fallback_to_legacy = false;
1152 $existing = wpbc_form_config_load( $form_name, $owner_user_id, 'published', $is_fallback_to_legacy );
1153 if ( ! empty( $existing ) ) {
1154 wp_send_json_error(
1155 array(
1156 'code' => 'already_exists',
1157 'message' => __( 'Form key already exists. Please choose another.', 'booking' ),
1158 )
1159 );
1160 }
1161
1162 $template = array();
1163 $structure_arr = array();
1164 $settings_arr = array();
1165 $engine = 'bfb';
1166 $engine_version = '1.0';
1167 $advanced_form = '';
1168 $content_form = '';
1169
1170 // Try to load template only when requested.
1171 if ( ! $is_blank ) {
1172
1173 // 1) Prefer user-owned template (MU) if exists.
1174 if ( $owner_user_id > 0 ) {
1175 $template = wpbc_form_config_load( $template_form_name, $owner_user_id, 'template' );
1176 }
1177
1178 // 2) Fallback to global template.
1179 if ( empty( $template ) ) {
1180 $template = wpbc_form_config_load( $template_form_name, 0, 'template' );
1181 }
1182
1183 // 3) If still missing (template deleted), fallback to standard if it exists.
1184 if ( empty( $template ) ) {
1185 $template = wpbc_form_config_load( 'standard', $owner_user_id );
1186 }
1187
1188 // If still nothing, create blank.
1189 if ( empty( $template ) ) {
1190 $is_blank = true;
1191 }
1192 }
1193
1194 if ( $is_blank ) {
1195
1196 // Blank form seed.
1197 $structure_arr = wpbc_bfb__get_blank_structure_seed();
1198 $settings_arr = wpbc_bfb__normalize_settings_array( array() );
1199
1200 // IMPORTANT: blank forms start in Builder sync mode (Builder -> Advanced).
1201 if ( empty( $settings_arr['bfb_options'] ) || ! is_array( $settings_arr['bfb_options'] ) ) {
1202 $settings_arr['bfb_options'] = array();
1203 }
1204 $settings_arr['bfb_options']['advanced_mode_source'] = 'builder';
1205
1206 $engine = 'bfb';
1207 $engine_version = '1.0';
1208 $advanced_form = '';
1209 $content_form = '';
1210
1211 } else {
1212
1213 // Clone structure from template.
1214 if ( ! empty( $template['structure_json'] ) ) {
1215 $tmp = json_decode( $template['structure_json'], true );
1216 if ( is_array( $tmp ) ) {
1217 $structure_arr = $tmp;
1218 }
1219 }
1220
1221 // Clone settings from template.
1222 $settings_arr = wpbc_bfb__normalize_settings_array( isset( $template['settings'] ) ? $template['settings'] : array() );
1223
1224 $engine = ! empty( $template['engine'] ) ? (string) $template['engine'] : 'bfb';
1225 $engine_version = ! empty( $template['engine_version'] ) ? (string) $template['engine_version'] : '1.0';
1226
1227 $advanced_form = isset( $template['advanced_form'] ) ? (string) $template['advanced_form'] : '';
1228 $content_form = isset( $template['content_form'] ) ? (string) $template['content_form'] : '';
1229 }
1230
1231 $form_config = array(
1232 'form_name' => $form_name,
1233 'engine' => $engine,
1234 'engine_version' => $engine_version,
1235 'structure_json' => wpbc_form_config__encode_json( $structure_arr ),
1236 'settings' => $settings_arr,
1237 'advanced_form' => $advanced_form,
1238 'content_form' => $content_form,
1239 'owner_user_id' => $owner_user_id,
1240
1241 'title' => $title,
1242 'description' => $description,
1243 'picture_url' => $image_url,
1244
1245 'scope' => ( $owner_user_id > 0 ) ? 'user' : 'global',
1246 'status' => 'published',
1247 'is_default' => 0,
1248 'booking_resource_id' => null,
1249 );
1250
1251 $sync_legacy = false;
1252
1253 $booking_form_id = wpbc_form_config_save( $form_config, array( 'sync_legacy' => (bool) $sync_legacy ) );
1254
1255 if ( ! $booking_form_id ) {
1256 wp_send_json_error(
1257 array(
1258 'code' => 'create_failed',
1259 'message' => __( 'Error creating booking form.', 'booking' ),
1260 )
1261 );
1262 }
1263
1264 wp_send_json_success(
1265 array(
1266 'booking_form_id' => $booking_form_id,
1267 'form_name' => $form_name,
1268 )
1269 );
1270 }
1271 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_CREATE_FORM_CONFIG', 'wpbc_bfb_ajax_create_form_config' );
1272
1273
1274 /**
1275 * Handle AJAX request: list booking forms for current user (and optionally global ones).
1276 *
1277 * Security:
1278 * - Verifies wpbc_bfb_form_list nonce (sent as 'nonce').
1279 * - Requires current_user_can( wpbc_bfb_get_manage_cap() ).
1280 *
1281 * Expects POST:
1282 * - nonce : string Nonce for 'wpbc_bfb_form_list'.
1283 * - include_global : 0|1 If 1, include global forms (owner_user_id=0/NULL) in addition to user-owned.
1284 * - status : string Default 'published'. Allowed: published|preview|draft|archived|template
1285 * - search : string Optional filter by title/slug/description
1286 * - limit : int Optional max rows (default 20, max 500)
1287 * - page : int Optional page number, starts from 1
1288 *
1289 * Response (JSON):
1290 * - success: true|false
1291 * - data: { forms: [ ... ] }
1292 *
1293 * @since 11.0.0
1294 *
1295 * @return void
1296 */
1297 function wpbc_bfb_ajax_list_forms() {
1298
1299 global $wpdb;
1300
1301 if ( ! check_ajax_referer( 'wpbc_bfb_form_list', 'nonce', false ) ) {
1302 wp_send_json_error( array(
1303 'code' => 'invalid_nonce',
1304 'message' => __( 'Security check failed.', 'booking' ),
1305 ) );
1306 }
1307
1308 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
1309 wp_send_json_error( array(
1310 'code' => 'forbidden',
1311 'message' => __( 'You are not allowed to list booking forms.', 'booking' ),
1312 ) );
1313 }
1314
1315 // Allow global forms ONLY when listing templates.
1316 // Templates usually live as global rows (owner_user_id = 0 / NULL).
1317 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1318 $include_global = ( isset( $_POST['include_global'] ) && '1' === (string) wp_unslash( $_POST['include_global'] ) );
1319
1320 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1321 $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published';
1322 if ( '' === $status ) {
1323 $status = 'published';
1324 }
1325
1326 $allowed_statuses = array( 'published', 'preview', 'draft', 'archived', 'template' );
1327 if ( ! in_array( $status, $allowed_statuses, true ) ) {
1328 $status = 'published';
1329 }
1330
1331 // Security policy: include_global is allowed only for templates.
1332 if ( 'template' !== $status ) {
1333 $include_global = false;
1334 }
1335
1336 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
1337
1338 // Pagination.
1339 $page = isset( $_POST['page'] ) ? absint( wp_unslash( $_POST['page'] ) ) : 1;
1340 if ( $page <= 0 ) {
1341 $page = 1;
1342 }
1343
1344 $limit = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 20;
1345 if ( $limit <= 0 ) {
1346 $limit = 20;
1347 }
1348 if ( $limit > 500 ) {
1349 $limit = 500;
1350 }
1351
1352 $offset = ( $page - 1 ) * $limit;
1353 if ( $offset < 0 ) {
1354 $offset = 0;
1355 }
1356
1357 // MU owner logic (same as save/load/create).
1358 $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
1359
1360 $table = $wpdb->prefix . 'booking_form_structures';
1361
1362 // Base WHERE.
1363 $where_sql = " WHERE status = %s ";
1364 $where_args = array( $status );
1365
1366 // Owner/global logic.
1367 if ( $owner_user_id > 0 ) {
1368 if ( $include_global ) {
1369 $where_sql .= " AND ( owner_user_id = %d OR owner_user_id = 0 OR owner_user_id IS NULL ) ";
1370 $where_args[] = $owner_user_id;
1371 } else {
1372 $where_sql .= " AND owner_user_id = %d ";
1373 $where_args[] = $owner_user_id;
1374 }
1375 } else {
1376 // Non-MU (or super admin context): treat as global rows.
1377 $where_sql .= " AND ( owner_user_id = 0 OR owner_user_id IS NULL ) ";
1378 }
1379
1380 // Search filter. Supports OR search by configured separator (default "~"): "time~duration~slots"
1381 if ( '' !== $search ) {
1382
1383 $terms = wpbc_bfb__split_search_terms_by_or_separator( $search, 5 );
1384
1385 if ( empty( $terms ) ) {
1386 // No usable terms after splitting.
1387 } elseif ( 1 === count( $terms ) ) {
1388
1389 $like = '%' . $wpdb->esc_like( $terms[0] ) . '%';
1390 $where_sql .= " AND ( form_slug LIKE %s OR title LIKE %s OR description LIKE %s ) ";
1391 $where_args[] = $like;
1392 $where_args[] = $like;
1393 $where_args[] = $like;
1394
1395 } else {
1396
1397 $or_groups = array();
1398
1399 foreach ( $terms as $term ) {
1400
1401 $or_groups[] = "( form_slug LIKE %s OR title LIKE %s OR description LIKE %s )";
1402
1403 $like = '%' . $wpdb->esc_like( $term ) . '%';
1404 $where_args[] = $like;
1405 $where_args[] = $like;
1406 $where_args[] = $like;
1407 }
1408
1409 $where_sql .= " AND ( " . implode( ' OR ', $or_groups ) . " ) ";
1410 }
1411 }
1412
1413 // Order:
1414 // - prefer user-owned rows first (when include_global + owner_user_id > 0)
1415 // - default forms first
1416 // - newest first
1417 $order_sql = " ORDER BY is_default DESC, updated_at DESC, version DESC, booking_form_id DESC ";
1418
1419 if ( $owner_user_id > 0 && $include_global ) {
1420 $order_sql = " ORDER BY ( owner_user_id = " . intval( $owner_user_id ) . " ) DESC, is_default DESC, updated_at DESC, version DESC, booking_form_id DESC ";
1421 }
1422
1423 $limit_plus_one = $limit + 1;
1424
1425 $sql = "SELECT booking_form_id, form_slug, title, description, picture_url, updated_at, owner_user_id, status, scope, is_default, version
1426 FROM {$table}
1427 {$where_sql}
1428 {$order_sql}
1429 LIMIT " . intval( $limit_plus_one ) . ' OFFSET ' . intval( $offset );
1430
1431 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1432 $rows = $wpdb->get_results( $wpdb->prepare( $sql, $where_args ) );
1433
1434 $has_more = ( count( (array) $rows ) > $limit );
1435 if ( $has_more ) {
1436 $rows = array_slice( (array) $rows, 0, $limit );
1437 }
1438
1439 $forms = array();
1440
1441 // If include_global + owner_user_id > 0: dedupe by slug, prefer owner over global.
1442 $seen_by_slug = array();
1443
1444 foreach ( (array) $rows as $r ) {
1445
1446 $slug = isset( $r->form_slug ) ? (string) $r->form_slug : '';
1447 if ( '' === $slug ) {
1448 continue;
1449 }
1450
1451 if ( $owner_user_id > 0 && $include_global ) {
1452 if ( isset( $seen_by_slug[ $slug ] ) ) {
1453 continue;
1454 }
1455 $seen_by_slug[ $slug ] = true;
1456 }
1457
1458
1459 $row_owner_user_id = isset( $r->owner_user_id ) ? absint( $r->owner_user_id ) : 0;
1460 $row_status = isset( $r->status ) ? (string) $r->status : '';
1461 $row_is_default = isset( $r->is_default ) ? absint( $r->is_default ) : 0;
1462
1463 $raw_picture_url = isset( $r->picture_url ) ? (string) $r->picture_url : '';
1464
1465 $final_picture_url = ( 'template' === $row_status ) ? wpbc_bfb_resolve_picture_url( $raw_picture_url ) : $raw_picture_url;
1466
1467 $can_delete = wpbc_bfb__can_delete_template_in_current_context(
1468 $slug,
1469 $row_owner_user_id,
1470 $owner_user_id,
1471 $row_is_default,
1472 $row_status
1473 );
1474
1475 $forms[] = array(
1476 'booking_form_id' => isset( $r->booking_form_id ) ? (int) $r->booking_form_id : 0,
1477 'form_slug' => $slug,
1478 'title' => isset( $r->title ) ? (string) $r->title : '',
1479 'description' => isset( $r->description ) ? (string) $r->description : '',
1480 'picture_url' => $final_picture_url,
1481 'updated_at' => isset( $r->updated_at ) ? (string) $r->updated_at : '',
1482 'owner_user_id' => $row_owner_user_id,
1483 'status' => $row_status,
1484 'scope' => isset( $r->scope ) ? (string) $r->scope : '',
1485 'is_default' => $row_is_default,
1486 'version' => isset( $r->version ) ? (int) $r->version : 0,
1487 'can_delete' => $can_delete ? 1 : 0,
1488 );
1489 }
1490
1491 wp_send_json_success( array(
1492 'forms' => $forms,
1493 'count' => count( $forms ),
1494 'page' => $page,
1495 'limit' => $limit,
1496 'has_more' => $has_more,
1497 ) );
1498 }
1499 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_LIST_FORMS', 'wpbc_bfb_ajax_list_forms' );
1500
1501
1502 /**
1503 * Handle AJAX request: delete a TEMPLATE FormConfig.
1504 *
1505 * Security:
1506 * - Verifies wpbc_bfb_form_delete nonce (or list nonce fallback).
1507 * - Requires current_user_can( wpbc_bfb_get_manage_cap() ).
1508 *
1509 * Expects POST:
1510 * - nonce : string Nonce for delete/list action.
1511 * - form_name : string Template slug/key to delete.
1512 *
1513 * Behaviour:
1514 * - Deletes ONLY template rows for the given slug.
1515 * - In MultiUser mode: a regular user can delete ONLY their own templates.
1516 * - Global templates shown to regular MU users are NOT deletable.
1517 *
1518 * Response (JSON):
1519 * - success: true|false
1520 * - data: {
1521 * form_name: string,
1522 * deleted: int
1523 * }
1524 *
1525 * @since 11.0.0
1526 *
1527 * @return void
1528 */
1529 function wpbc_bfb_ajax_delete_template_config() {
1530 global $wpdb;
1531
1532 if ( ! wpbc_bfb__verify_delete_request_nonce() ) {
1533 wp_send_json_error(
1534 array(
1535 'code' => 'invalid_nonce',
1536 'message' => __( 'Security check failed.', 'booking' ),
1537 )
1538 );
1539 }
1540
1541 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
1542 wp_send_json_error(
1543 array(
1544 'code' => 'forbidden',
1545 'message' => __( 'You are not allowed to delete templates.', 'booking' ),
1546 )
1547 );
1548 }
1549
1550 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
1551 $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
1552 if ( '' === $form_name ) {
1553 wp_send_json_error(
1554 array(
1555 'code' => 'invalid_form_name',
1556 'message' => __( 'Template key is required.', 'booking' ),
1557 )
1558 );
1559 }
1560
1561 if ( 'standard' === $form_name ) {
1562 wp_send_json_error(
1563 array(
1564 'code' => 'reserved',
1565 'message' => __( 'This template cannot be deleted.', 'booking' ),
1566 )
1567 );
1568 }
1569
1570 $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
1571
1572 /**
1573 * Filter whether deletion of a specific template is allowed.
1574 *
1575 * @since 11.0.0
1576 *
1577 * @param bool $is_allowed Default true.
1578 * @param string $form_name Template slug/key.
1579 * @param int $owner_user_id Owner user id in MU (0 for global).
1580 */
1581 $is_allowed = apply_filters( 'wpbc_bfb_delete_template_is_allowed', true, $form_name, $owner_user_id );
1582 if ( ! $is_allowed ) {
1583 wp_send_json_error(
1584 array(
1585 'code' => 'not_allowed',
1586 'message' => __( 'Deletion is not allowed for this template.', 'booking' ),
1587 )
1588 );
1589 }
1590
1591 $table = $wpdb->prefix . 'booking_form_structures';
1592
1593 $where_sql = " WHERE form_slug = %s AND status = %s ";
1594 $where_args = array( $form_name, 'template' );
1595
1596 if ( $owner_user_id > 0 ) {
1597 $where_sql .= " AND owner_user_id = %d ";
1598 $where_args[] = $owner_user_id;
1599 } else {
1600 $where_sql .= " AND ( owner_user_id = 0 OR owner_user_id IS NULL ) ";
1601 }
1602
1603 $sql = "SELECT booking_form_id, is_default, owner_user_id
1604 FROM {$table}
1605 {$where_sql}
1606 ORDER BY updated_at DESC, version DESC, booking_form_id DESC
1607 LIMIT 1";
1608
1609 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1610 $row = $wpdb->get_row( $wpdb->prepare( $sql, $where_args ) );
1611
1612 if ( empty( $row ) ) {
1613 wp_send_json_error(
1614 array(
1615 'code' => 'not_found',
1616 'message' => __( 'Template not found.', 'booking' ),
1617 )
1618 );
1619 }
1620
1621 if ( 1 === absint( $row->is_default ) ) {
1622 wp_send_json_error(
1623 array(
1624 'code' => 'reserved',
1625 'message' => __( 'This template cannot be deleted.', 'booking' ),
1626 )
1627 );
1628 }
1629
1630 $delete_sql = "DELETE FROM {$table} {$where_sql}";
1631
1632 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1633 $deleted = $wpdb->query( $wpdb->prepare( $delete_sql, $where_args ) );
1634
1635 if ( false === $deleted ) {
1636 wp_send_json_error(
1637 array(
1638 'code' => 'delete_failed',
1639 'message' => __( 'Error deleting template.', 'booking' ) . ( ! empty( $wpdb->last_error ) ? ' ' . $wpdb->last_error : '' ),
1640 )
1641 );
1642 }
1643
1644 wp_send_json_success(
1645 array(
1646 'form_name' => $form_name,
1647 /* translators: 1: template name */
1648 'message' => sprintf( __( 'Template %s deleted.', 'booking' ), "'" . $form_name . "'" ) . ' [' . absint( $deleted ) . ']',
1649 'deleted' => absint( $deleted ),
1650 )
1651 );
1652 }
1653 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_DELETE_TEMPLATE_CONFIG', 'wpbc_bfb_ajax_delete_template_config' );
1654
1655
1656 /**
1657 * Handle AJAX request: delete a custom FormConfig.
1658 *
1659 * Security:
1660 * - Verifies wpbc_bfb_form_delete nonce (sent as 'nonce').
1661 * - Requires current_user_can( wpbc_bfb_get_manage_cap() ).
1662 *
1663 * Expects POST:
1664 * - nonce : string Nonce for 'wpbc_bfb_form_delete'.
1665 * - form_name : string Custom form slug/key to delete (required).
1666 *
1667 * Behaviour:
1668 * - Blocks deletion of reserved/default forms (e.g. 'standard' or is_default=1).
1669 * - In MultiUser mode: a regular user can delete ONLY their own forms.
1670 * - Deletes ALL rows for this form_slug (all statuses/versions), excluding scope='template'.
1671 *
1672 * Response (JSON):
1673 * - success: true|false
1674 * - data: {
1675 * form_name: string,
1676 * deleted: int
1677 * }
1678 *
1679 * @since 11.0.0
1680 *
1681 * @return void
1682 */
1683 function wpbc_bfb_ajax_delete_form_config() {
1684 global $wpdb;
1685
1686 if ( ! wpbc_bfb__verify_delete_request_nonce() ) {
1687 wp_send_json_error(
1688 array(
1689 'code' => 'invalid_nonce',
1690 'message' => __( 'Security check failed.', 'booking' ),
1691 )
1692 );
1693 }
1694
1695 if ( ! current_user_can( wpbc_bfb_get_manage_cap() ) ) {
1696 wp_send_json_error(
1697 array(
1698 'code' => 'forbidden',
1699 'message' => __( 'You are not allowed to delete booking forms.', 'booking' ),
1700 )
1701 );
1702 }
1703
1704 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
1705 $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
1706 if ( '' === $form_name ) {
1707 wp_send_json_error(
1708 array(
1709 'code' => 'invalid_form_name',
1710 'message' => __( 'Form key is required.', 'booking' ),
1711 )
1712 );
1713 }
1714
1715 // Block reserved key.
1716 if ( 'standard' === $form_name ) {
1717 wp_send_json_error(
1718 array(
1719 'code' => 'reserved',
1720 'message' => __( 'This form cannot be deleted.', 'booking' ),
1721 )
1722 );
1723 }
1724
1725 // MU owner logic (same approach as save/load/create/list).
1726 $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id();
1727
1728 /**
1729 * Filter whether deletion of a specific form is allowed.
1730 *
1731 * @since 11.0.0
1732 *
1733 * @param bool $is_allowed Default true.
1734 * @param string $form_name Form slug/key.
1735 * @param int $owner_user_id Owner user id in MU (0 for global).
1736 */
1737 $is_allowed = apply_filters( 'wpbc_bfb_delete_form_is_allowed', true, $form_name, $owner_user_id );
1738 if ( ! $is_allowed ) {
1739 wp_send_json_error(
1740 array(
1741 'code' => 'not_allowed',
1742 'message' => __( 'Deletion is not allowed for this form.', 'booking' ),
1743 )
1744 );
1745 }
1746
1747 $table = $wpdb->prefix . 'booking_form_structures';
1748
1749 // ---------------------------------------------------------------------------------
1750 // Check existence + protect default/template.
1751 // ---------------------------------------------------------------------------------
1752 $where_sql = " WHERE form_slug = %s ";
1753 $where_args = array( $form_name );
1754
1755 if ( $owner_user_id > 0 ) {
1756 $where_sql .= " AND owner_user_id = %d ";
1757 $where_args[] = $owner_user_id;
1758 } else {
1759 $where_sql .= " AND ( owner_user_id = 0 OR owner_user_id IS NULL ) ";
1760 }
1761
1762 // Exclude template scope rows from selection checks as well.
1763 $where_sql .= " AND ( scope IS NULL OR scope <> %s ) ";
1764 $where_args[] = 'template';
1765
1766 // Exclude template scope rows from selection checks as well.
1767 $where_sql .= " AND ( status IS NULL OR status <> %s ) ";
1768 $where_args[] = 'template';
1769
1770 $sql = "SELECT booking_form_id, is_default, scope, status
1771 FROM {$table}
1772 {$where_sql}
1773 ORDER BY updated_at DESC, version DESC, booking_form_id DESC
1774 LIMIT 1";
1775
1776 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
1777 $row = $wpdb->get_row( $wpdb->prepare( $sql, $where_args ) );
1778
1779 if ( empty( $row ) ) {
1780 wp_send_json_error(
1781 array(
1782 'code' => 'not_found',
1783 'message' => __( 'Booking form not found.', 'booking' ),
1784 )
1785 );
1786 }
1787
1788 $is_default = isset( $row->is_default ) ? absint( $row->is_default ) : 0;
1789 if ( 1 === $is_default ) {
1790 wp_send_json_error(
1791 array(
1792 'code' => 'reserved',
1793 'message' => __( 'This form cannot be deleted.', 'booking' ),
1794 )
1795 );
1796 }
1797
1798 $scope = isset( $row->scope ) ? (string) $row->scope : '';
1799 $status = isset( $row->status ) ? (string) $row->status : '';
1800 if ( ( 'template' === $scope ) || ( 'template' === $status ) ) {
1801 wp_send_json_error(
1802 array(
1803 'code' => 'reserved',
1804 'message' => __( 'Template forms cannot be deleted.', 'booking' ),
1805 )
1806 );
1807 }
1808
1809 // ---------------------------------------------------------------------------------
1810 // Delete ALL rows for this slug/owner (all statuses/versions), excluding templates.
1811 // ---------------------------------------------------------------------------------
1812 $delete_where_sql = " WHERE form_slug = %s ";
1813 $delete_where_args = array( $form_name );
1814
1815 if ( $owner_user_id > 0 ) {
1816 $delete_where_sql .= " AND owner_user_id = %d ";
1817 $delete_where_args[] = $owner_user_id;
1818 } else {
1819 $delete_where_sql .= " AND ( owner_user_id = 0 OR owner_user_id IS NULL ) ";
1820 }
1821
1822 $delete_where_sql .= " AND ( scope IS NULL OR scope <> %s ) ";
1823 $delete_where_args[] = 'template';
1824 $delete_where_sql .= " AND ( status IS NULL OR status <> %s ) ";
1825 $delete_where_args[] = 'template';
1826
1827 $delete_sql = "DELETE FROM {$table} {$delete_where_sql}";
1828
1829 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
1830 $deleted = $wpdb->query( $wpdb->prepare( $delete_sql, $delete_where_args ) );
1831
1832 if ( false === $deleted ) {
1833 wp_send_json_error(
1834 array(
1835 'code' => 'delete_failed',
1836 'message' => __( 'Error deleting booking form.', 'booking' ) . ( ! empty( $wpdb->last_error ) ? ' ' . $wpdb->last_error : '' ),
1837 )
1838 );
1839 }
1840
1841 wp_send_json_success(
1842 array(
1843 'form_name' => $form_name,
1844 /* translators: 1: template name */
1845 'message' => sprintf( __( 'Booking form %s deleted.', 'booking' ), "'" . $form_name . "'" ) . ' [' . absint( $deleted ) . ']',
1846 'deleted' => absint( $deleted ),
1847 )
1848 );
1849 }
1850 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_DELETE_FORM_CONFIG', 'wpbc_bfb_ajax_delete_form_config' );
1851