| @@ -140,8 +140,23 @@ | ||
| 140 | 140 | '--wpbc-bfb-col-ai', |
| 141 | 141 | '--wpbc-bfb-col-gap', |
| 142 | 142 | '--wpbc-bfb-col-ac', |
| 143 | 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', | |
| 144 | 159 | '--wpbc-bfb-form-background', |
| 145 | 160 | '--wpbc-bfb-form-border-color', |
| 146 | 161 | '--wpbc-bfb-form-border-width', |
| 147 | 162 | '--wpbc-bfb-form-border-radius', |
| @@ -227,9 +242,9 @@ | ||
| 227 | 242 | $allowed_tags['p']['name'] = true; |
| 228 | 243 | } |
| 229 | 244 | |
| 230 | 245 | // Extra attributes for layout/structure wrappers. |
| 231 | - foreach ( array( 'div', 'span', 'hr' ) as $tag ) { | |
| 246 | + foreach ( array( 'div', 'span', 'hr' ) as $tag ) { | |
| 232 | 247 | if ( ! isset( $allowed_tags[ $tag ] ) ) { |
| 233 | 248 | $allowed_tags[ $tag ] = array(); |
| 234 | 249 | } |
| 235 | 250 | $allowed_tags[ $tag ]['data-bfb-type'] = true; |
| @@ -234,10 +249,19 @@ | ||
| 234 | 249 | } |
| 235 | 250 | $allowed_tags[ $tag ]['data-bfb-type'] = true; |
| 236 | 251 | $allowed_tags[ $tag ]['data-orientation'] = true; |
| 237 | 252 | $allowed_tags[ $tag ]['name'] = true; |
| 238 | - $allowed_tags[ $tag ]['aria-orientation'] = true; | |
| 239 | - } | |
| 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; | |
| 240 | 264 | |
| 241 | 265 | // Temporarily allow extra inline style properties for BFB. |
| 242 | 266 | add_filter( 'safe_style_css', 'wpbc_bfb_safe_style_props_filter', 10, 1 ); |
| 243 | 267 | |
| @@ -464,14 +488,18 @@ | ||
| 464 | 488 | |
| 465 | 489 | $custom_options = function_exists( 'wpbc_bfb_settings__get_custom_form_style_options' ) |
| 466 | 490 | ? wpbc_bfb_settings__get_custom_form_style_options( $preview_form_style ) |
| 467 | 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(); | |
| 468 | 495 | |
| 469 | 496 | return array_merge( |
| 470 | 497 | array( |
| 471 | 498 | 'booking_form_style' => $style, |
| 472 | 499 | ), |
| 473 | - $custom_options | |
| 500 | + $custom_options, | |
| 501 | + $accent_options | |
| 474 | 502 | ); |
| 475 | 503 | } |
| 476 | 504 | |
| 477 | 505 | /** |
| @@ -709,22 +737,43 @@ | ||
| 709 | 737 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 710 | 738 | $preview_form_style_raw = isset( $_POST['preview_form_style'] ) ? wp_unslash( $_POST['preview_form_style'] ) : ''; |
| 711 | 739 | |
| 712 | 740 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 713 | - $content_form_raw = isset( $_POST['content_form'] ) ? wp_unslash( $_POST['content_form'] ) : ''; | |
| 714 | - $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 ); | |
| 715 | 751 | |
| 716 | - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 717 | - $advanced_form_raw = isset( $_POST['advanced_form'] ) ? wp_unslash( $_POST['advanced_form'] ) : ''; | |
| 718 | - $advanced_form = wpbc_bfb_sanitize_form_text( $advanced_form_raw ); | |
| 719 | 752 | |
| 720 | - | |
| 721 | 753 | // Validate structure JSON. |
| 722 | 754 | $structure_arr = json_decode( $structure_raw, true ); |
| 723 | - if ( ! is_array( $structure_arr ) ) { | |
| 724 | - wp_send_json_error( array( 'code' => 'invalid_structure', 'message' => __( 'Form structure is not a valid JSON object.', 'booking' ) ) ); | |
| 725 | - } | |
| 726 | - | |
| 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 | + | |
| 727 | 776 | // Settings JSON (normalized to the ONLY supported schema). |
| 728 | 777 | $settings_arr = wpbc_bfb__normalize_settings_array( $settings_raw ); |
| 729 | 778 | $preview_form_style = wpbc_bfb__normalize_preview_form_style( $preview_form_style_raw ); |
| 730 | 779 | // $advanced_mode_source = ( isset( $settings_arr['bfb_options']['advanced_mode_source'] ) ) ? (string) $settings_arr['bfb_options']['advanced_mode_source'] : 'builder'; |
| @@ -861,9 +910,17 @@ | ||
| 861 | 910 | if ( $return_preview_url && 'preview' === $status && class_exists( 'WPBC_BFB_Preview_Service' ) ) { |
| 862 | 911 | |
| 863 | 912 | $preview_service = WPBC_BFB_Preview_Service::get_instance(); |
| 864 | 913 | |
| 865 | - $res = $preview_service->create_preview_session( $preview_form_id, wpbc_get_current_user_id(), $structure_arr, $form_name, $advanced_form, $content_form, $preview_form_style ); | |
| 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 | + ); | |
| 866 | 923 | |
| 867 | 924 | if ( is_array( $res ) && ! empty( $res['preview_url'] ) ) { |
| 868 | 925 | $preview_url = (string) $res['preview_url']; |
| 869 | 926 | $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : ''; |
| @@ -869,27 +926,9 @@ | ||
| 869 | 926 | $preview_token = ! empty( $res['token'] ) ? (string) $res['token'] : ''; |
| 870 | 927 | } |
| 871 | 928 | } |
| 872 | 929 | |
| 873 | - $setup_step_saved = false; | |
| 874 | - $setup_step = isset( $_POST['wpbc_setup_step'] ) ? sanitize_key( wp_unslash( $_POST['wpbc_setup_step'] ) ) : ''; | |
| 875 | - if ( ! empty( $setup_step ) && class_exists( 'WPBC_SETUP_WIZARD_STEPS' ) ) { | |
| 876 | - $setup_steps = new WPBC_SETUP_WIZARD_STEPS(); | |
| 877 | - $steps_arr = $setup_steps->get_steps_arr(); | |
| 878 | - if ( function_exists( 'wpbc_setup_wizard__detect_step_from_admin_url' ) ) { | |
| 879 | - $referer_step = wpbc_setup_wizard__detect_step_from_admin_url( wp_get_referer() ); | |
| 880 | - if ( ! empty( $referer_step ) && isset( $steps_arr[ $referer_step ] ) ) { | |
| 881 | - $setup_step = $referer_step; | |
| 882 | - } | |
| 883 | - } | |
| 884 | - if ( isset( $steps_arr[ $setup_step ] ) ) { | |
| 885 | - $setup_steps->db__set_step_as_saved( $setup_step, true ); | |
| 886 | - $setup_steps->db__save_current_step_name( $setup_step ); | |
| 887 | - $setup_step_saved = true; | |
| 888 | - } | |
| 889 | - } | |
| 890 | - | |
| 891 | - wp_send_json_success( | |
| 930 | + wp_send_json_success( | |
| 892 | 931 | array( |
| 893 | 932 | 'booking_form_id' => $booking_form_id, |
| 894 | 933 | 'form_name' => $form_name, |
| 895 | 934 | 'engine' => $engine, |
| @@ -898,10 +937,9 @@ | ||
| 898 | 937 | 'token' => $preview_token, |
| 899 | 938 | 'title' => isset( $form_config['title'] ) ? (string) $form_config['title'] : '', |
| 900 | 939 | 'description' => isset( $form_config['description'] ) ? (string) $form_config['description'] : '', |
| 901 | 940 | 'picture_url' => isset( $form_config['picture_url'] ) ? (string) $form_config['picture_url'] : '', |
| 902 | - 'setup_step_saved' => $setup_step_saved, | |
| 903 | - ) | |
| 941 | + ) | |
| 904 | 942 | ); |
| 905 | 943 | |
| 906 | 944 | } |
| 907 | 945 | add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_SAVE_FORM_CONFIG', 'wpbc_bfb_ajax_save_form_config' ); |
| @@ -1270,32 +1308,101 @@ | ||
| 1270 | 1308 | } |
| 1271 | 1309 | add_action( 'wp_ajax_' . 'WPBC_AJX_BFB_CREATE_FORM_CONFIG', 'wpbc_bfb_ajax_create_form_config' ); |
| 1272 | 1310 | |
| 1273 | 1311 | |
| 1274 | -/** | |
| 1275 | - * Handle AJAX request: list booking forms for current user (and optionally global ones). | |
| 1276 | - * | |
| 1277 | - * Security: | |
| 1278 | - * - Verifies wpbc_bfb_form_list nonce (sent as 'nonce'). | |
| 1279 | - * - Requires current_user_can( wpbc_bfb_get_manage_cap() ). | |
| 1280 | - * | |
| 1281 | - * Expects POST: | |
| 1282 | - * - nonce : string Nonce for 'wpbc_bfb_form_list'. | |
| 1283 | - * - include_global : 0|1 If 1, include global forms (owner_user_id=0/NULL) in addition to user-owned. | |
| 1284 | - * - status : string Default 'published'. Allowed: published|preview|draft|archived|template | |
| 1285 | - * - search : string Optional filter by title/slug/description | |
| 1286 | - * - limit : int Optional max rows (default 20, max 500) | |
| 1287 | - * - page : int Optional page number, starts from 1 | |
| 1288 | - * | |
| 1289 | - * Response (JSON): | |
| 1290 | - * - success: true|false | |
| 1291 | - * - data: { forms: [ ... ] } | |
| 1292 | - * | |
| 1293 | - * @since 11.0.0 | |
| 1294 | - * | |
| 1295 | - * @return void | |
| 1296 | - */ | |
| 1297 | -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() { | |
| 1298 | 1405 | |
| 1299 | 1406 | global $wpdb; |
| 1300 | 1407 | |
| 1301 | 1408 | if ( ! check_ajax_referer( 'wpbc_bfb_form_list', 'nonce', false ) ) { |
| @@ -1409,18 +1516,35 @@ | ||
| 1409 | 1516 | $where_sql .= " AND ( " . implode( ' OR ', $or_groups ) . " ) "; |
| 1410 | 1517 | } |
| 1411 | 1518 | } |
| 1412 | 1519 | |
| 1413 | - // Order: | |
| 1414 | - // - prefer user-owned rows first (when include_global + owner_user_id > 0) | |
| 1415 | - // - default forms first | |
| 1416 | - // - newest first | |
| 1417 | - $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'] ); | |
| 1418 | 1546 | |
| 1419 | - if ( $owner_user_id > 0 && $include_global ) { | |
| 1420 | - $order_sql = " ORDER BY ( owner_user_id = " . intval( $owner_user_id ) . " ) DESC, is_default DESC, updated_at DESC, version DESC, booking_form_id DESC "; | |
| 1421 | - } | |
| 1422 | - | |
| 1423 | 1547 | $limit_plus_one = $limit + 1; |
| 1424 | 1548 | |
| 1425 | 1549 | $sql = "SELECT booking_form_id, form_slug, title, description, picture_url, updated_at, owner_user_id, status, scope, is_default, version |
| 1426 | 1550 | FROM {$table} |
| @@ -1428,9 +1552,9 @@ | ||
| 1428 | 1552 | {$order_sql} |
| 1429 | 1553 | LIMIT " . intval( $limit_plus_one ) . ' OFFSET ' . intval( $offset ); |
| 1430 | 1554 | |
| 1431 | 1555 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 1432 | - $rows = $wpdb->get_results( $wpdb->prepare( $sql, $where_args ) ); | |
| 1556 | + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $query_args ) ); | |
| 1433 | 1557 | |
| 1434 | 1558 | $has_more = ( count( (array) $rows ) > $limit ); |
| 1435 | 1559 | if ( $has_more ) { |
| 1436 | 1560 | $rows = array_slice( (array) $rows, 0, $limit ); |