PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | includes/page-form-builder/ajax/bfb-ajax.php +271 -110 11.3 → 11.9 View file →
@@ -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,27 +926,9 @@
828 926 $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : '';
829 927 }
830 928 }
831 929
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(
930 + wp_send_json_success(
851 931 array(
852 932 'booking_form_id' => $booking_form_id,
853 933 'form_name' => $form_name,
854 934 'engine' => $engine,
@@ -857,10 +937,9 @@
857 937 'token' => $preview_token,
858 938 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : '',
859 939 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '',
860 940 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : '',
861 - 'setup_step_saved' => $setup_step_saved,
862 - )
941 + )
863 942 );
864 943
865 944 }
866 945 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_SAVE_FORM_CONFIG', 'wpbc_bfb_ajax_save_form_config' );
@@ -930,13 +1009,9 @@
930 1009 $form_name = isset( $_POST['form_name'] ) ? sanitize_text_field( wp_unslash( $_POST['form_name'] ) ) : '';
931 1010 if ( '' === $form_name ) {
932 1011 $form_name = 'standard';
933 1012 }
934 - if ( ( defined( 'WPBC_BFB_DEBUG__FORM_NAME' ) ) && ( ! empty( WPBC_BFB_DEBUG__FORM_NAME ) ) ) {
935 - $form_name = WPBC_BFB_DEBUG__FORM_NAME;
936 - }
937 1013
938 -
939 1014 $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'published';
940 1015 $allowed_statuses = array( 'published', 'preview', 'template' );
941 1016 if ( ! in_array( $status, $allowed_statuses, true ) ) {
942 1017 $status = 'published';
@@ -975,10 +1050,10 @@
975 1050 $structure = $tmp;
976 1051 }
977 1052 }
978 1053
979 - // Fallback notice only when no structure exists at all.
980 - 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 ) {
981 1056
982 1057 $structure = array(
983 1058 array(
984 1059 'page' => 1,
@@ -988,9 +1063,9 @@
988 1063 'data' => array(
989 1064 'id' => 'static_text_legacy_notice_1',
990 1065 'type' => 'static_text',
991 1066 'usage_key' => 'static_text',
992 - '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' ),
993 1068 'tag' => 'p',
994 1069 'align' => 'center',
995 1070 'bold' => 1,
996 1071 'italic' => 0,
@@ -1007,9 +1082,9 @@
1007 1082 'data' => array(
1008 1083 'id' => 'static_text_legacy_notice_2',
1009 1084 'type' => 'static_text',
1010 1085 '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' ),
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' ),
1012 1087 'tag' => 'p',
1013 1088 'align' => 'center',
1014 1089 'bold' => 0,
1015 1090 'italic' => 0,
@@ -1204,9 +1279,9 @@
1204 1279 'title' => $title,
1205 1280 'description' => $description,
1206 1281 'picture_url' => $image_url,
1207 1282
1208 - 'scope' => 'global',
1283 + 'scope' => ( $owner_user_id > 0 ) ? 'user' : 'global',
1209 1284 'status' => 'published',
1210 1285 'is_default' => 0,
1211 1286 'booking_resource_id' => null,
1212 1287 );
@@ -1233,32 +1308,101 @@
1233 1308 }
1234 1309 add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_CREATE_FORM_CONFIG', 'wpbc_bfb_ajax_create_form_config' );
1235 1310
1236 1311
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() {
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() {
1261 1405
1262 1406 global $wpdb;
1263 1407
1264 1408 if ( ! check_ajax_referer( 'wpbc_bfb_form_list', 'nonce', false ) ) {
@@ -1372,18 +1516,35 @@
1372 1516 $where_sql .= " AND ( " . implode( ' OR ', $or_groups ) . " ) ";
1373 1517 }
1374 1518 }
1375 1519
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 ";
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'] );
1381 1546
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 1547 $limit_plus_one = $limit + 1;
1387 1548
1388 1549 $sql = "SELECT booking_form_id, form_slug, title, description, picture_url, updated_at, owner_user_id, status, scope, is_default, version
1389 1550 FROM {$table}
@@ -1391,9 +1552,9 @@
1391 1552 {$order_sql}
1392 1553 LIMIT " . intval( $limit_plus_one ) . ' OFFSET ' . intval( $offset );
1393 1554
1394 1555 // 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 ) );
1556 + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $query_args ) );
1396 1557
1397 1558 $has_more = ( count( (array) $rows ) > $limit );
1398 1559 if ( $has_more ) {
1399 1560 $rows = array_slice( (array) $rows, 0, $limit );