| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | 3 | * AJAX controller for Booking Form Builder (BFB) FormConfig. |
| 4 | 4 | * |
| @@ -56,13 +56,8 @@ | ||
| 56 | 56 | if ( ! defined( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR_URL' ) ) { |
| 57 | 57 | define( 'WPBC_BFB_TEMPLATE_SEARCH_OR_SEPARATOR_URL', '^' ); |
| 58 | 58 | } |
| 59 | 59 | |
| 60 | -// == DEBUG == | |
| 61 | -if ( ! defined( 'WPBC_BFB_DEBUG__FORM_NAME' ) ) { | |
| 62 | -// define( 'WPBC_BFB_DEBUG__FORM_NAME', 'wizard-1' ); | |
| 63 | -} | |
| 64 | - | |
| 65 | 60 | // == Helpers == ======================================================================================================= |
| 66 | 61 | |
| 67 | 62 | /** |
| 68 | 63 | * Get capability required to manage booking forms in the Builder. |
| @@ -142,13 +137,34 @@ | ||
| 142 | 137 | '--wpbc-bfb-col-dir', |
| 143 | 138 | '--wpbc-bfb-col-wrap', |
| 144 | 139 | '--wpbc-bfb-col-jc', |
| 145 | 140 | '--wpbc-bfb-col-ai', |
| 146 | - '--wpbc-bfb-col-gap', | |
| 147 | - '--wpbc-bfb-col-ac', | |
| 148 | - '--wpbc-bfb-col-aself', | |
| 149 | - '--wpbc-col-min', | |
| 150 | - ); | |
| 141 | + '--wpbc-bfb-col-gap', | |
| 142 | + '--wpbc-bfb-col-ac', | |
| 143 | + '--wpbc-bfb-col-aself', | |
| 144 | + '--wpbc-bfb-col-padding', | |
| 145 | + '--wpbc-bfb-col-margin', | |
| 146 | + '--wpbc-bfb-col-padding-top', | |
| 147 | + '--wpbc-bfb-col-padding-right', | |
| 148 | + '--wpbc-bfb-col-padding-bottom', | |
| 149 | + '--wpbc-bfb-col-padding-left', | |
| 150 | + '--wpbc-bfb-col-margin-top', | |
| 151 | + '--wpbc-bfb-col-margin-right', | |
| 152 | + '--wpbc-bfb-col-margin-bottom', | |
| 153 | + '--wpbc-bfb-col-margin-left', | |
| 154 | + '--wpbc-bfb-col-max-width', | |
| 155 | + '--wpbc-bfb-col-max-height', | |
| 156 | + '--wpbc-bfb-col-overflow', | |
| 157 | + '--wpbc-bfb-col-overflow-x', | |
| 158 | + '--wpbc-bfb-col-overflow-y', | |
| 159 | + '--wpbc-bfb-form-background', | |
| 160 | + '--wpbc-bfb-form-border-color', | |
| 161 | + '--wpbc-bfb-form-border-width', | |
| 162 | + '--wpbc-bfb-form-border-radius', | |
| 163 | + '--wpbc-bfb-form-padding', | |
| 164 | + '--wpbc-bfb-form-box-shadow', | |
| 165 | + '--wpbc-col-min', | |
| 166 | + ); | |
| 151 | 167 | |
| 152 | 168 | /** |
| 153 | 169 | * Filter extra safe CSS properties for BFB inline styles. |
| 154 | 170 | * |
| @@ -226,9 +242,9 @@ | ||
| 226 | 242 | $allowed_tags['p']['name'] = true; |
| 227 | 243 | } |
| 228 | 244 | |
| 229 | 245 | // Extra attributes for layout/structure wrappers. |
| 230 | - foreach ( array( 'div', 'span', 'hr' ) as $tag ) { | |
| 246 | + foreach ( array( 'div', 'span', 'hr' ) as $tag ) { | |
| 231 | 247 | if ( ! isset( $allowed_tags[ $tag ] ) ) { |
| 232 | 248 | $allowed_tags[ $tag ] = array(); |
| 233 | 249 | } |
| 234 | 250 | $allowed_tags[ $tag ]['data-bfb-type'] = true; |
| @@ -233,10 +249,19 @@ | ||
| 233 | 249 | } |
| 234 | 250 | $allowed_tags[ $tag ]['data-bfb-type'] = true; |
| 235 | 251 | $allowed_tags[ $tag ]['data-orientation'] = true; |
| 236 | 252 | $allowed_tags[ $tag ]['name'] = true; |
| 237 | - $allowed_tags[ $tag ]['aria-orientation'] = true; | |
| 238 | - } | |
| 253 | + $allowed_tags[ $tag ]['aria-orientation'] = true; | |
| 254 | + } | |
| 255 | + | |
| 256 | + // Appointment Form Builder control: permit only its declarative action. | |
| 257 | + if ( ! isset( $allowed_tags['button'] ) ) { | |
| 258 | + $allowed_tags['button'] = array(); | |
| 259 | + } | |
| 260 | + $allowed_tags['button']['type'] = true; | |
| 261 | + $allowed_tags['button']['class'] = true; | |
| 262 | + $allowed_tags['button']['id'] = true; | |
| 263 | + $allowed_tags['button']['data-wpbc-appointment-action'] = true; | |
| 239 | 264 | |
| 240 | 265 | // Temporarily allow extra inline style properties for BFB. |
| 241 | 266 | add_filter( 'safe_style_css', 'wpbc_bfb_safe_style_props_filter', 10, 1 ); |
| 242 | 267 | |
| @@ -398,9 +423,9 @@ | ||
| 398 | 423 | * @param mixed $settings |
| 399 | 424 | * |
| 400 | 425 | * @return array |
| 401 | 426 | */ |
| 402 | -function wpbc_bfb__normalize_settings_array( $settings ) { | |
| 427 | +function wpbc_bfb__normalize_settings_array( $settings ) { | |
| 403 | 428 | |
| 404 | 429 | if ( is_string( $settings ) && '' !== $settings ) { |
| 405 | 430 | $tmp = json_decode( $settings, true ); |
| 406 | 431 | if ( is_array( $tmp ) ) { |
| @@ -411,16 +436,20 @@ | ||
| 411 | 436 | if ( ! is_array( $settings ) ) { |
| 412 | 437 | $settings = array(); |
| 413 | 438 | } |
| 414 | 439 | |
| 415 | - if ( empty( $settings['options'] ) || ! is_array( $settings['options'] ) ) { | |
| 416 | - $settings['options'] = array(); | |
| 417 | - } | |
| 440 | + if ( empty( $settings['options'] ) || ! is_array( $settings['options'] ) ) { | |
| 441 | + $settings['options'] = array(); | |
| 442 | + } | |
| 443 | + | |
| 444 | + if ( function_exists( 'wpbc_bfb_settings__strip_form_style_options_from_form_settings' ) ) { | |
| 445 | + $settings = wpbc_bfb_settings__strip_form_style_options_from_form_settings( $settings ); | |
| 446 | + } | |
| 447 | + | |
| 448 | + if ( empty( $settings['css_vars'] ) || ! is_array( $settings['css_vars'] ) ) { | |
| 449 | + $settings['css_vars'] = array(); | |
| 450 | + } | |
| 418 | 451 | |
| 419 | - if ( empty( $settings['css_vars'] ) || ! is_array( $settings['css_vars'] ) ) { | |
| 420 | - $settings['css_vars'] = array(); | |
| 421 | - } | |
| 422 | - | |
| 423 | 452 | if ( empty( $settings['bfb_options'] ) || ! is_array( $settings['bfb_options'] ) ) { |
| 424 | 453 | $settings['bfb_options'] = array(); |
| 425 | 454 | } |
| 426 | 455 | |
| @@ -429,13 +458,53 @@ | ||
| 429 | 458 | $src = 'auto'; |
| 430 | 459 | } |
| 431 | 460 | $settings['bfb_options']['advanced_mode_source'] = $src; |
| 432 | 461 | |
| 433 | - return $settings; | |
| 434 | -} | |
| 435 | - | |
| 436 | -/** | |
| 437 | - * Check whether template key means "blank form". | |
| 462 | + return $settings; | |
| 463 | +} | |
| 464 | + | |
| 465 | +/** | |
| 466 | + * Normalize preview-only global Form Style override. | |
| 467 | + * | |
| 468 | + * @param mixed $preview_form_style Raw JSON string or array. | |
| 469 | + * @return array | |
| 470 | + */ | |
| 471 | +function wpbc_bfb__normalize_preview_form_style( $preview_form_style ) { | |
| 472 | + | |
| 473 | + if ( is_string( $preview_form_style ) && '' !== trim( $preview_form_style ) ) { | |
| 474 | + $decoded = json_decode( $preview_form_style, true ); | |
| 475 | + if ( is_array( $decoded ) ) { | |
| 476 | + $preview_form_style = $decoded; | |
| 477 | + } | |
| 478 | + } | |
| 479 | + | |
| 480 | + if ( ! is_array( $preview_form_style ) ) { | |
| 481 | + return array(); | |
| 482 | + } | |
| 483 | + | |
| 484 | + $style = isset( $preview_form_style['booking_form_style'] ) ? $preview_form_style['booking_form_style'] : ''; | |
| 485 | + $style = function_exists( 'wpbc_bfb_settings__sanitize_form_style' ) | |
| 486 | + ? wpbc_bfb_settings__sanitize_form_style( $style ) | |
| 487 | + : sanitize_key( (string) $style ); | |
| 488 | + | |
| 489 | + $custom_options = function_exists( 'wpbc_bfb_settings__get_custom_form_style_options' ) | |
| 490 | + ? wpbc_bfb_settings__get_custom_form_style_options( $preview_form_style ) | |
| 491 | + : array(); | |
| 492 | + $accent_options = function_exists( 'wpbc_bfb_settings__get_form_accent_options' ) | |
| 493 | + ? wpbc_bfb_settings__get_form_accent_options( $preview_form_style ) | |
| 494 | + : array(); | |
| 495 | + | |
| 496 | + return array_merge( | |
| 497 | + array( | |
| 498 | + 'booking_form_style' => $style, | |
| 499 | + ), | |
| 500 | + $custom_options, | |
| 501 | + $accent_options | |
| 502 | + ); | |
| 503 | +} | |
| 504 | + | |
| 505 | +/** | |
| 506 | + * Check whether template key means "blank form". | |
| 438 | 507 | * |
| 439 | 508 | * @param string $template_form_name |
| 440 | 509 | * |
| 441 | 510 | * @return bool |
| @@ -640,11 +709,8 @@ | ||
| 640 | 709 | $form_name = isset( $_POST['form_name'] ) ? wpbc_bfb__sanitize_form_slug( wp_unslash( $_POST['form_name'] ) ) : ''; |
| 641 | 710 | if ( '' === $form_name ) { |
| 642 | 711 | $form_name = 'standard'; |
| 643 | 712 | } |
| 644 | - if ( ( defined( 'WPBC_BFB_DEBUG__FORM_NAME' ) ) && ( ! empty( WPBC_BFB_DEBUG__FORM_NAME ) ) ) { | |
| 645 | - $form_name = WPBC_BFB_DEBUG__FORM_NAME; | |
| 646 | - } | |
| 647 | 713 | |
| 648 | 714 | $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published'; |
| 649 | 715 | $allowed_statuses = array( 'published', 'preview', 'template' ); |
| 650 | 716 | if ( ! in_array( $status, $allowed_statuses, true ) ) { |
| @@ -665,30 +731,54 @@ | ||
| 665 | 731 | $engine_version = isset( $_POST['engine_version'] ) ? sanitize_text_field( wp_unslash( $_POST['engine_version'] ) ) : '1.0'; |
| 666 | 732 | |
| 667 | 733 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 668 | 734 | $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'] ) : ''; | |
| 735 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 736 | + $settings_raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; | |
| 737 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 738 | + $preview_form_style_raw = isset( $_POST['preview_form_style'] ) ? wp_unslash( $_POST['preview_form_style'] ) : ''; | |
| 671 | 739 | |
| 672 | 740 | // 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 ); | |
| 741 | + $content_form_raw = isset( $_POST['content_form'] ) && is_scalar( $_POST['content_form'] ) | |
| 742 | + ? wp_unslash( $_POST['content_form'] ) | |
| 743 | + : ''; | |
| 744 | + $content_form = wpbc_bfb_sanitize_form_text( $content_form_raw ); | |
| 745 | + | |
| 746 | + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 747 | + $advanced_form_raw = isset( $_POST['advanced_form'] ) && is_scalar( $_POST['advanced_form'] ) | |
| 748 | + ? wp_unslash( $_POST['advanced_form'] ) | |
| 749 | + : ''; | |
| 750 | + $advanced_form = wpbc_bfb_sanitize_form_text( $advanced_form_raw ); | |
| 675 | 751 | |
| 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 | 752 | |
| 680 | - | |
| 681 | 753 | // Validate structure JSON. |
| 682 | 754 | $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 | - } | |
| 755 | + if ( ! is_array( $structure_arr ) ) { | |
| 756 | + wp_send_json_error( array( 'code' => 'invalid_structure', 'message' => __( 'Form structure is not a valid JSON object.', 'booking' ) ) ); | |
| 757 | + } | |
| 758 | + $preview_structure_arr = $structure_arr; | |
| 759 | + | |
| 760 | + /** | |
| 761 | + * Filter and sanitize a decoded Form Builder structure before persistence. | |
| 762 | + * | |
| 763 | + * Field packs may normalize only their own stored properties. Callbacks must | |
| 764 | + * return the complete structure and must not perform persistence or output. | |
| 765 | + * | |
| 766 | + * @since 11.8.4 | |
| 767 | + * | |
| 768 | + * @param array $structure_arr Decoded Form Builder structure. | |
| 769 | + */ | |
| 770 | + $structure_arr = apply_filters( 'wpbc_bfb_sanitize_structure_before_save', $structure_arr ); | |
| 771 | + | |
| 772 | + if ( ! is_array( $structure_arr ) ) { | |
| 773 | + wp_send_json_error( array( 'code' => 'invalid_structure', 'message' => __( 'Form structure could not be normalized.', 'booking' ) ) ); | |
| 774 | + } | |
| 775 | + | |
| 776 | + // Settings JSON (normalized to the ONLY supported schema). | |
| 777 | + $settings_arr = wpbc_bfb__normalize_settings_array( $settings_raw ); | |
| 778 | + $preview_form_style = wpbc_bfb__normalize_preview_form_style( $preview_form_style_raw ); | |
| 779 | + // $advanced_mode_source = ( isset( $settings_arr['bfb_options']['advanced_mode_source'] ) ) ? (string) $settings_arr['bfb_options']['advanced_mode_source'] : 'builder'; | |
| 686 | 780 | |
| 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 | 781 | // Check if owner of this form is "Regular User" in MU. |
| 692 | 782 | $owner_user_id = WPBC_FE_Custom_Form_Helper::wpbc_mu__get_current__owner_user_id(); |
| 693 | 783 | |
| 694 | 784 | $form_config = array( |
| @@ -699,9 +789,9 @@ | ||
| 699 | 789 | 'settings' => $settings_arr, |
| 700 | 790 | 'advanced_form' => $advanced_form, |
| 701 | 791 | 'content_form' => $content_form, |
| 702 | 792 | 'owner_user_id' => $owner_user_id, |
| 703 | - 'scope' => 'global', | |
| 793 | + 'scope' => ( $owner_user_id > 0 ) ? 'user' : 'global', | |
| 704 | 794 | 'status' => $status, |
| 705 | 795 | 'is_default' => ( ( 'standard' === $form_name ) && ( 'template' !== $status ) ) ? 1 : 0, |
| 706 | 796 | 'booking_resource_id' => null, |
| 707 | 797 | ); |
| @@ -820,9 +910,17 @@ | ||
| 820 | 910 | if ( $return_preview_url && 'preview' === $status && class_exists( 'WPBC_BFB_Preview_Service' ) ) { |
| 821 | 911 | |
| 822 | 912 | $preview_service = WPBC_BFB_Preview_Service::get_instance(); |
| 823 | 913 | |
| 824 | - $res = $preview_service->create_preview_session( $preview_form_id, wpbc_get_current_user_id(), $structure_arr, $form_name, $advanced_form, $content_form ); | |
| 914 | + $res = $preview_service->create_preview_session( | |
| 915 | + $preview_form_id, | |
| 916 | + get_current_user_id(), | |
| 917 | + $preview_structure_arr, | |
| 918 | + $form_name, | |
| 919 | + $advanced_form_raw, | |
| 920 | + $content_form_raw, | |
| 921 | + $preview_form_style | |
| 922 | + ); | |
| 825 | 923 | |
| 826 | 924 | if ( is_array( $res ) && ! empty( $res['preview_url'] ) ) { |
| 827 | 925 | $preview_url = (string) $res['preview_url']; |
| 828 | 926 | $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : ''; |
| @@ -828,9 +926,9 @@ | ||
| 828 | 926 | $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : ''; |
| 829 | 927 | } |
| 830 | 928 | } |
| 831 | 929 | |
| 832 | - wp_send_json_success( | |
| 930 | + wp_send_json_success( | |
| 833 | 931 | array( |
| 834 | 932 | 'booking_form_id' => $booking_form_id, |
| 835 | 933 | 'form_name' => $form_name, |
| 836 | 934 | 'engine' => $engine, |
| @@ -839,9 +937,9 @@ | ||
| 839 | 937 | 'token' => $preview_token, |
| 840 | 938 | 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : '', |
| 841 | 939 | 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '', |
| 842 | 940 | 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : '', |
| 843 | - ) | |
| 941 | + ) | |
| 844 | 942 | ); |
| 845 | 943 | |
| 846 | 944 | } |
| 847 | 945 | add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_SAVE_FORM_CONFIG', 'wpbc_bfb_ajax_save_form_config' ); |
| @@ -911,13 +1009,9 @@ | ||
| 911 | 1009 | $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : ''; |
| 912 | 1010 | if ( '' === $form_name ) { |
| 913 | 1011 | $form_name = 'standard'; |
| 914 | 1012 | } |
| 915 | - if ( ( defined( 'WPBC_BFB_DEBUG__FORM_NAME' ) ) && ( ! empty( WPBC_BFB_DEBUG__FORM_NAME ) ) ) { | |
| 916 | - $form_name = WPBC_BFB_DEBUG__FORM_NAME; | |
| 917 | - } | |
| 918 | 1013 | |
| 919 | - | |
| 920 | 1014 | $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published'; |
| 921 | 1015 | $allowed_statuses = array( 'published', 'preview', 'template' ); |
| 922 | 1016 | if ( ! in_array( $status, $allowed_statuses, true ) ) { |
| 923 | 1017 | $status = 'published'; |
| @@ -956,10 +1050,10 @@ | ||
| 956 | 1050 | $structure = $tmp; |
| 957 | 1051 | } |
| 958 | 1052 | } |
| 959 | 1053 | |
| 960 | - // Fallback notice only when no structure exists at all. | |
| 961 | - if ( empty( $structure ) && in_array( $engine, array( 'legacy_simple', 'legacy_advanced' ), true ) ) { | |
| 1054 | + // Advanced Mode notice only when no visual Builder structure exists. | |
| 1055 | + if ( empty( $structure ) && 'advanced_mode' === $engine ) { | |
| 962 | 1056 | |
| 963 | 1057 | $structure = array( |
| 964 | 1058 | array( |
| 965 | 1059 | 'page' => 1, |
| @@ -969,9 +1063,9 @@ | ||
| 969 | 1063 | 'data' => array( |
| 970 | 1064 | 'id' => 'static_text_legacy_notice_1', |
| 971 | 1065 | 'type' => 'static_text', |
| 972 | 1066 | 'usage_key' => 'static_text', |
| 973 | - 'text' => __( 'This form is currently configured in Advanced Form mode only.', 'booking' ), | |
| 1067 | + 'text' => __( 'This imported form is currently configured in Advanced Form mode only.', 'booking' ), | |
| 974 | 1068 | 'tag' => 'p', |
| 975 | 1069 | 'align' => 'center', |
| 976 | 1070 | 'bold' => 1, |
| 977 | 1071 | 'italic' => 0, |
| @@ -988,9 +1082,9 @@ | ||
| 988 | 1082 | 'data' => array( |
| 989 | 1083 | 'id' => 'static_text_legacy_notice_2', |
| 990 | 1084 | 'type' => 'static_text', |
| 991 | 1085 | 'usage_key' => 'static_text', |
| 992 | - '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' ), | |
| 1086 | + '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' ), | |
| 993 | 1087 | 'tag' => 'p', |
| 994 | 1088 | 'align' => 'center', |
| 995 | 1089 | 'bold' => 0, |
| 996 | 1090 | 'italic' => 0, |
| @@ -1185,9 +1279,9 @@ | ||
| 1185 | 1279 | 'title' => $title, |
| 1186 | 1280 | 'description' => $description, |
| 1187 | 1281 | 'picture_url' => $image_url, |
| 1188 | 1282 | |
| 1189 | - 'scope' => 'global', | |
| 1283 | + 'scope' => ( $owner_user_id > 0 ) ? 'user' : 'global', | |
| 1190 | 1284 | 'status' => 'published', |
| 1191 | 1285 | 'is_default' => 0, |
| 1192 | 1286 | 'booking_resource_id' => null, |
| 1193 | 1287 | ); |
| @@ -1214,32 +1308,101 @@ | ||
| 1214 | 1308 | } |
| 1215 | 1309 | add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_CREATE_FORM_CONFIG', 'wpbc_bfb_ajax_create_form_config' ); |
| 1216 | 1310 | |
| 1217 | 1311 | |
| 1218 | -/** | |
| 1219 | - * Handle AJAX request: list booking forms for current user (and optionally global ones). | |
| 1220 | - * | |
| 1221 | - * Security: | |
| 1222 | - * - Verifies wpbc_bfb_form_list nonce (sent as 'nonce'). | |
| 1223 | - * - Requires current_user_can( wpbc_bfb_get_manage_cap() ). | |
| 1224 | - * | |
| 1225 | - * Expects POST: | |
| 1226 | - * - nonce : string Nonce for 'wpbc_bfb_form_list'. | |
| 1227 | - * - include_global : 0|1 If 1, include global forms (owner_user_id=0/NULL) in addition to user-owned. | |
| 1228 | - * - status : string Default 'published'. Allowed: published|preview|draft|archived|template | |
| 1229 | - * - search : string Optional filter by title/slug/description | |
| 1230 | - * - limit : int Optional max rows (default 20, max 500) | |
| 1231 | - * - page : int Optional page number, starts from 1 | |
| 1232 | - * | |
| 1233 | - * Response (JSON): | |
| 1234 | - * - success: true|false | |
| 1235 | - * - data: { forms: [ ... ] } | |
| 1236 | - * | |
| 1237 | - * @since 11.0.0 | |
| 1238 | - * | |
| 1239 | - * @return void | |
| 1240 | - */ | |
| 1241 | -function wpbc_bfb_ajax_list_forms() { | |
| 1312 | +/** | |
| 1313 | + * Build optional adjacency-aware ordering for the template library. | |
| 1314 | + * | |
| 1315 | + * Domains may register stable before/after slug pairs through | |
| 1316 | + * `wpbc_bfb_template_library_adjacencies`. The list endpoint keeps every pair | |
| 1317 | + * together at the existing target template's chronological position without | |
| 1318 | + * placing domain slugs or other business knowledge in the shared controller. | |
| 1319 | + * | |
| 1320 | + * @param string $table_name Trusted Booking Form structures table name. | |
| 1321 | + * | |
| 1322 | + * @return array SQL fragments and ordered prepare arguments. | |
| 1323 | + */ | |
| 1324 | +function wpbc_bfb_get_template_library_order_clauses( $table_name ) { | |
| 1325 | + | |
| 1326 | + $adjacencies = apply_filters( 'wpbc_bfb_template_library_adjacencies', array() ); | |
| 1327 | + | |
| 1328 | + if ( ! is_array( $adjacencies ) ) { | |
| 1329 | + $adjacencies = array(); | |
| 1330 | + } | |
| 1331 | + | |
| 1332 | + $effective_updated_at_cases = array(); | |
| 1333 | + $adjacency_rank_cases = array(); | |
| 1334 | + $effective_updated_at_args = array(); | |
| 1335 | + $adjacency_rank_args = array(); | |
| 1336 | + | |
| 1337 | + foreach ( $adjacencies as $adjacency ) { | |
| 1338 | + if ( ! is_array( $adjacency ) ) { | |
| 1339 | + continue; | |
| 1340 | + } | |
| 1341 | + | |
| 1342 | + $before_slug = isset( $adjacency['before'] ) ? sanitize_title( (string) $adjacency['before'] ) : ''; | |
| 1343 | + $after_slug = isset( $adjacency['after'] ) ? sanitize_title( (string) $adjacency['after'] ) : ''; | |
| 1344 | + | |
| 1345 | + if ( '' === $before_slug || '' === $after_slug || $before_slug === $after_slug ) { | |
| 1346 | + continue; | |
| 1347 | + } | |
| 1348 | + | |
| 1349 | + $effective_updated_at_cases[] = "WHEN form_slug = %s THEN COALESCE( | |
| 1350 | + ( SELECT MAX( wpbc_adjacent_target.updated_at ) | |
| 1351 | + FROM {$table_name} AS wpbc_adjacent_target | |
| 1352 | + WHERE wpbc_adjacent_target.form_slug = %s | |
| 1353 | + AND wpbc_adjacent_target.status = 'template' | |
| 1354 | + AND ( wpbc_adjacent_target.owner_user_id = 0 OR wpbc_adjacent_target.owner_user_id IS NULL ) ), | |
| 1355 | + updated_at | |
| 1356 | + )"; | |
| 1357 | + $effective_updated_at_args[] = $before_slug; | |
| 1358 | + $effective_updated_at_args[] = $after_slug; | |
| 1359 | + | |
| 1360 | + $adjacency_rank_cases[] = 'WHEN form_slug = %s THEN 2 WHEN form_slug = %s THEN 1'; | |
| 1361 | + $adjacency_rank_args[] = $before_slug; | |
| 1362 | + $adjacency_rank_args[] = $after_slug; | |
| 1363 | + } | |
| 1364 | + | |
| 1365 | + if ( empty( $effective_updated_at_cases ) ) { | |
| 1366 | + return array( | |
| 1367 | + 'effective_updated_at_sql' => 'updated_at', | |
| 1368 | + 'adjacency_rank_sql' => '', | |
| 1369 | + 'args' => array(), | |
| 1370 | + ); | |
| 1371 | + } | |
| 1372 | + | |
| 1373 | + return array( | |
| 1374 | + 'effective_updated_at_sql' => 'CASE ' . implode( ' ', $effective_updated_at_cases ) . ' ELSE updated_at END', | |
| 1375 | + 'adjacency_rank_sql' => 'CASE ' . implode( ' ', $adjacency_rank_cases ) . ' ELSE 0 END', | |
| 1376 | + 'args' => array_merge( $effective_updated_at_args, $adjacency_rank_args ), | |
| 1377 | + ); | |
| 1378 | +} | |
| 1379 | + | |
| 1380 | +/** | |
| 1381 | + * Handle AJAX request: list booking forms for current user (and optionally global ones). | |
| 1382 | + * | |
| 1383 | + * Security: | |
| 1384 | + * - Verifies wpbc_bfb_form_list nonce (sent as 'nonce'). | |
| 1385 | + * - Requires current_user_can( wpbc_bfb_get_manage_cap() ). | |
| 1386 | + * | |
| 1387 | + * Expects POST: | |
| 1388 | + * - nonce : string Nonce for 'wpbc_bfb_form_list'. | |
| 1389 | + * - include_global : 0|1 If 1, include global forms (owner_user_id=0/NULL) in addition to user-owned. | |
| 1390 | + * - status : string Default 'published'. Allowed: published|preview|draft|archived|template | |
| 1391 | + * - search : string Optional filter by title/slug/description | |
| 1392 | + * - limit : int Optional max rows (default 20, max 500) | |
| 1393 | + * - page : int Optional page number, starts from 1 | |
| 1394 | + * | |
| 1395 | + * Response (JSON): | |
| 1396 | + * - success: true|false | |
| 1397 | + * - data: { forms: [ ... ] } | |
| 1398 | + * | |
| 1399 | + * @since 11.0.0 | |
| 1400 | + * | |
| 1401 | + * @return void | |
| 1402 | + */ | |
| 1403 | + | |
| 1404 | +function wpbc_bfb_ajax_list_forms() { | |
| 1242 | 1405 | |
| 1243 | 1406 | global $wpdb; |
| 1244 | 1407 | |
| 1245 | 1408 | if ( ! check_ajax_referer( 'wpbc_bfb_form_list', 'nonce', false ) ) { |
| @@ -1353,18 +1516,35 @@ | ||
| 1353 | 1516 | $where_sql .= " AND ( " . implode( ' OR ', $or_groups ) . " ) "; |
| 1354 | 1517 | } |
| 1355 | 1518 | } |
| 1356 | 1519 | |
| 1357 | - // Order: | |
| 1358 | - // - prefer user-owned rows first (when include_global + owner_user_id > 0) | |
| 1359 | - // - default forms first | |
| 1360 | - // - newest first | |
| 1361 | - $order_sql = " ORDER BY is_default DESC, updated_at DESC, version DESC, booking_form_id DESC "; | |
| 1520 | + // Order: | |
| 1521 | + // - prefer user-owned rows first (when include_global + owner_user_id > 0) | |
| 1522 | + // - default forms first | |
| 1523 | + // - preserve registered template adjacency at the target template's position | |
| 1524 | + // - newest first | |
| 1525 | + $template_order_clauses = array( | |
| 1526 | + 'effective_updated_at_sql' => 'updated_at', | |
| 1527 | + 'adjacency_rank_sql' => '', | |
| 1528 | + 'args' => array(), | |
| 1529 | + ); | |
| 1530 | + | |
| 1531 | + if ( 'template' === $status ) { | |
| 1532 | + $template_order_clauses = wpbc_bfb_get_template_library_order_clauses( $table ); | |
| 1533 | + } | |
| 1534 | + | |
| 1535 | + $effective_updated_at_sql = $template_order_clauses['effective_updated_at_sql']; | |
| 1536 | + $adjacency_rank_order_sql = '' !== $template_order_clauses['adjacency_rank_sql'] | |
| 1537 | + ? ', ' . $template_order_clauses['adjacency_rank_sql'] . ' DESC' | |
| 1538 | + : ''; | |
| 1539 | + $order_sql = " ORDER BY is_default DESC, {$effective_updated_at_sql} DESC{$adjacency_rank_order_sql}, version DESC, booking_form_id DESC "; | |
| 1540 | + | |
| 1541 | + if ( $owner_user_id > 0 && $include_global ) { | |
| 1542 | + $order_sql = " ORDER BY ( owner_user_id = " . intval( $owner_user_id ) . " ) DESC, is_default DESC, {$effective_updated_at_sql} DESC{$adjacency_rank_order_sql}, version DESC, booking_form_id DESC "; | |
| 1543 | + } | |
| 1544 | + | |
| 1545 | + $query_args = array_merge( $where_args, $template_order_clauses['args'] ); | |
| 1362 | 1546 | |
| 1363 | - if ( $owner_user_id > 0 && $include_global ) { | |
| 1364 | - $order_sql = " ORDER BY ( owner_user_id = " . intval( $owner_user_id ) . " ) DESC, is_default DESC, updated_at DESC, version DESC, booking_form_id DESC "; | |
| 1365 | - } | |
| 1366 | - | |
| 1367 | 1547 | $limit_plus_one = $limit + 1; |
| 1368 | 1548 | |
| 1369 | 1549 | $sql = "SELECT booking_form_id, form_slug, title, description, picture_url, updated_at, owner_user_id, status, scope, is_default, version |
| 1370 | 1550 | FROM {$table} |
| @@ -1372,9 +1552,9 @@ | ||
| 1372 | 1552 | {$order_sql} |
| 1373 | 1553 | LIMIT " . intval( $limit_plus_one ) . ' OFFSET ' . intval( $offset ); |
| 1374 | 1554 | |
| 1375 | 1555 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 1376 | - $rows = $wpdb->get_results( $wpdb->prepare( $sql, $where_args ) ); | |
| 1556 | + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $query_args ) ); | |
| 1377 | 1557 | |
| 1378 | 1558 | $has_more = ( count( (array) $rows ) > $limit ); |
| 1379 | 1559 | if ( $has_more ) { |
| 1380 | 1560 | $rows = array_slice( (array) $rows, 0, $limit ); |