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