PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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 / ajax / bfb-ajax.php

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

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