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 -91 11.1 → 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,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 );