| @@ -384,8 +384,26 @@ | ||
| 384 | 384 | } |
| 385 | 385 | } |
| 386 | 386 | |
| 387 | 387 | /** |
| 388 | + * Sanitize SVG markup. | |
| 389 | + * | |
| 390 | + * Uses the existing SVG allowlist from merchant_kses_allowed_tags() | |
| 391 | + * to preserve SVG elements while stripping unsafe content. | |
| 392 | + * | |
| 393 | + * @param mixed $value The raw SVG markup. | |
| 394 | + * | |
| 395 | + * @return string The sanitized SVG markup. | |
| 396 | + * | |
| 397 | + * @since 2.3.0 | |
| 398 | + */ | |
| 399 | +if ( ! function_exists( 'merchant_sanitize_svg' ) ) { | |
| 400 | + function merchant_sanitize_svg( $value ) { | |
| 401 | + return wp_kses( (string) $value, merchant_kses_allowed_tags( array(), false ) ); | |
| 402 | + } | |
| 403 | +} | |
| 404 | + | |
| 405 | +/** | |
| 388 | 406 | * Format a price for admin preview display. |
| 389 | 407 | * |
| 390 | 408 | * Uses wc_price() when WooCommerce is available, otherwise falls back to a plain |
| 391 | 409 | * currency symbol + number_format string. |
| @@ -504,8 +522,96 @@ | ||
| 504 | 522 | } |
| 505 | 523 | } |
| 506 | 524 | |
| 507 | 525 | /** |
| 526 | + * Get every descendant category slug of a given category term. | |
| 527 | + * | |
| 528 | + * Uses a single `child_of` term query, so the whole branch is returned in one | |
| 529 | + * call regardless of how deeply the tree is nested. | |
| 530 | + * | |
| 531 | + * @param int $term_id Parent product category term ID. | |
| 532 | + * | |
| 533 | + * @return string[] Descendant category slugs. Empty when the term has no children. | |
| 534 | + */ | |
| 535 | +if ( ! function_exists( 'merchant_get_category_descendant_slugs' ) ) { | |
| 536 | + function merchant_get_category_descendant_slugs( $term_id ) { | |
| 537 | + $descendants = get_terms( | |
| 538 | + array( | |
| 539 | + 'taxonomy' => 'product_cat', | |
| 540 | + 'child_of' => (int) $term_id, | |
| 541 | + 'fields' => 'slugs', | |
| 542 | + 'hide_empty' => false, | |
| 543 | + ) | |
| 544 | + ); | |
| 545 | + | |
| 546 | + if ( is_wp_error( $descendants ) || ! is_array( $descendants ) ) { | |
| 547 | + return array(); | |
| 548 | + } | |
| 549 | + | |
| 550 | + return $descendants; | |
| 551 | + } | |
| 552 | +} | |
| 553 | + | |
| 554 | +/** | |
| 555 | + * Expand category slugs to include all of their descendant categories. | |
| 556 | + * | |
| 557 | + * Products are only ever assigned to the categories picked for them, so a | |
| 558 | + * product sitting in "Hoodies" is not matched by a rule targeting its parent | |
| 559 | + * "Clothing". Expanding the parent into its whole branch makes that match work. | |
| 560 | + * | |
| 561 | + * Slugs that do not resolve to a category are passed through untouched, so an | |
| 562 | + * unknown slug never silently disappears from a rule. | |
| 563 | + * | |
| 564 | + * @param string[] $slugs Category slugs to expand. | |
| 565 | + * | |
| 566 | + * @return string[] The given slugs plus every descendant slug, de-duplicated. | |
| 567 | + */ | |
| 568 | +if ( ! function_exists( 'merchant_expand_categories_with_children' ) ) { | |
| 569 | + function merchant_expand_categories_with_children( $slugs ) { | |
| 570 | + $slugs = array_filter( array_map( 'strval', (array) $slugs ), 'strlen' ); | |
| 571 | + | |
| 572 | + if ( empty( $slugs ) ) { | |
| 573 | + return array(); | |
| 574 | + } | |
| 575 | + | |
| 576 | + $expanded = $slugs; | |
| 577 | + | |
| 578 | + foreach ( $slugs as $slug ) { | |
| 579 | + $term = get_term_by( 'slug', $slug, 'product_cat' ); | |
| 580 | + | |
| 581 | + if ( empty( $term ) || is_wp_error( $term ) ) { | |
| 582 | + continue; | |
| 583 | + } | |
| 584 | + | |
| 585 | + $expanded = array_merge( $expanded, merchant_get_category_descendant_slugs( $term->term_id ) ); | |
| 586 | + } | |
| 587 | + | |
| 588 | + return array_values( array_unique( $expanded ) ); | |
| 589 | + } | |
| 590 | +} | |
| 591 | + | |
| 592 | +/** | |
| 593 | + * Expand category slugs only when a campaign opted into subcategories. | |
| 594 | + * | |
| 595 | + * Call this at the point a campaign's category slugs are read, so matching | |
| 596 | + * behaviour stays byte-for-byte identical while the toggle is off. | |
| 597 | + * | |
| 598 | + * @param string[] $slugs Category slugs from the campaign. | |
| 599 | + * @param array<string, mixed> $settings Campaign settings holding the toggle. | |
| 600 | + * | |
| 601 | + * @return string[] Expanded slugs when the toggle is on, otherwise the input. | |
| 602 | + */ | |
| 603 | +if ( ! function_exists( 'merchant_maybe_expand_categories' ) ) { | |
| 604 | + function merchant_maybe_expand_categories( $slugs, $settings ) { | |
| 605 | + if ( empty( $settings['include_subcategories'] ) ) { | |
| 606 | + return (array) $slugs; | |
| 607 | + } | |
| 608 | + | |
| 609 | + return merchant_expand_categories_with_children( $slugs ); | |
| 610 | + } | |
| 611 | +} | |
| 612 | + | |
| 613 | +/** | |
| 508 | 614 | * Get the product tags. |
| 509 | 615 | */ |
| 510 | 616 | if ( ! function_exists( 'merchant_get_product_tags' ) ) { |
| 511 | 617 | function merchant_get_product_tags() { |
| @@ -766,10 +872,12 @@ | ||
| 766 | 872 | |
| 767 | 873 | $condition = $args['user_condition'] ?? 'all'; |
| 768 | 874 | |
| 769 | 875 | $is_exclusion_enabled = $args['user_exclusion_enabled'] ?? false; |
| 770 | - $excluded_customers = array_map( 'intval', $args['exclude_users'] ?? array() ); | |
| 771 | - $excluded_roles = $args['exclude_roles'] ?? array(); | |
| 876 | + $exclude_users_raw = $args['exclude_users'] ?? array(); | |
| 877 | + $excluded_customers = array_map( 'intval', is_array( $exclude_users_raw ) ? $exclude_users_raw : array() ); | |
| 878 | + $excluded_roles_raw = $args['exclude_roles'] ?? array(); | |
| 879 | + $excluded_roles = is_array( $excluded_roles_raw ) ? $excluded_roles_raw : array(); | |
| 772 | 880 | |
| 773 | 881 | switch ( $condition ) { |
| 774 | 882 | case 'all': |
| 775 | 883 | case '': |
| @@ -792,14 +900,16 @@ | ||
| 792 | 900 | if ( $is_exclusion_enabled && in_array( $customer_id, $excluded_customers, true ) ) { |
| 793 | 901 | return false; |
| 794 | 902 | } |
| 795 | 903 | |
| 796 | - $allowed_roles = $args['user_condition_roles'] ?? array(); | |
| 904 | + $allowed_roles_raw = $args['user_condition_roles'] ?? array(); | |
| 905 | + $allowed_roles = is_array( $allowed_roles_raw ) ? $allowed_roles_raw : array(); | |
| 797 | 906 | return in_array( $user_role, $allowed_roles, true ); |
| 798 | 907 | |
| 799 | 908 | case 'customers': |
| 800 | 909 | case 'users': |
| 801 | - $allowed_customers = array_map( 'intval', $args['user_condition_users'] ?? array() ); | |
| 910 | + $allowed_users_raw = $args['user_condition_users'] ?? array(); | |
| 911 | + $allowed_customers = array_map( 'intval', is_array( $allowed_users_raw ) ? $allowed_users_raw : array() ); | |
| 802 | 912 | return $is_logged_in && in_array( $customer_id, $allowed_customers, true ); |
| 803 | 913 | |
| 804 | 914 | default: |
| 805 | 915 | return false; |
| @@ -852,9 +962,9 @@ | ||
| 852 | 962 | } |
| 853 | 963 | |
| 854 | 964 | // Exclude categories |
| 855 | 965 | if ( in_array( $display_rule, array( 'all', 'all_products', 'categories', 'category', 'by_category' ), true ) ) { |
| 856 | - $excluded_categories_slugs = $args['excluded_categories'] ?? array(); | |
| 966 | + $excluded_categories_slugs = merchant_maybe_expand_categories( $args['excluded_categories'] ?? array(), $args ); | |
| 857 | 967 | |
| 858 | 968 | if ( ! empty( $excluded_categories_slugs ) && has_term( $excluded_categories_slugs, 'product_cat', $_product_id ) ) { |
| 859 | 969 | return true; |
| 860 | 970 | } |
| @@ -879,9 +989,9 @@ | ||
| 879 | 989 | } |
| 880 | 990 | |
| 881 | 991 | // Exclude categories with toggle |
| 882 | 992 | if ( ! empty( $args['exclude_categories_toggle'] ) && in_array( $display_rule, array( 'all', 'all_products', 'categories', 'category', 'by_category' ), true ) ) { |
| 883 | - $excluded_categories_slugs = $args['excluded_categories'] ?? array(); | |
| 993 | + $excluded_categories_slugs = merchant_maybe_expand_categories( $args['excluded_categories'] ?? array(), $args ); | |
| 884 | 994 | |
| 885 | 995 | if ( ! empty( $excluded_categories_slugs ) && has_term( $excluded_categories_slugs, 'product_cat', $_product_id ) ) { |
| 886 | 996 | return true; |
| 887 | 997 | } |
| @@ -1105,8 +1215,10 @@ | ||
| 1105 | 1215 | if ( Merchant_Buy_X_Get_Y::MODULE_ID === $module_id || Merchant_Pre_Orders::MODULE_ID === $module_id ) { |
| 1106 | 1216 | $key = 'rules'; |
| 1107 | 1217 | } elseif ( Merchant_Product_Labels::MODULE_ID === $module_id ) { |
| 1108 | 1218 | $key = 'labels'; |
| 1219 | + } elseif ( 'buy-now' === $module_id ) { | |
| 1220 | + $key = 'campaigns'; | |
| 1109 | 1221 | } else { |
| 1110 | 1222 | $key = 'offers'; |
| 1111 | 1223 | } |
| 1112 | 1224 | |