PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
← All changes | inc/helpers.php +391 -25 1.11.02.3.2 View file →
@@ -61,8 +61,9 @@
61 61 'stroke' => true,
62 62 'stroke-width' => true,
63 63 'stroke-linejoin' => true,
64 64 'clip-rule' => true,
65 + 'opacity' => true,
65 66 ),
66 67 'polyline' => array(
67 68 'points' => true,
68 69 'fill' => true,
@@ -383,8 +384,49 @@
383 384 }
384 385 }
385 386
386 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 +/**
406 + * Format a price for admin preview display.
407 + *
408 + * Uses wc_price() when WooCommerce is available, otherwise falls back to a plain
409 + * currency symbol + number_format string.
410 + *
411 + * @param float $price The price value.
412 + * @param string $currency Currency symbol fallback (used only when WC is unavailable).
413 + *
414 + * @return string Formatted price HTML.
415 + *
416 + * @since 2.2.5
417 + */
418 +if ( ! function_exists( 'merchant_preview_price' ) ) {
419 + function merchant_preview_price( $price, $currency = '$' ) {
420 + if ( function_exists( 'wc_price' ) ) {
421 + return wp_kses( wc_price( $price ), merchant_kses_allowed_tags( array( 'bdi' ) ) );
422 + }
423 +
424 + return esc_html( $currency ) . esc_html( number_format( (float) $price, 2 ) );
425 + }
426 +}
427 +
428 +/**
387 429 * Check if WooCommerce checkout page is being rendered by block.
388 430 * Since WooCommerce 8.3.0 the checkout page is rendered by block.
389 431 *
390 432 * @return bool
@@ -480,8 +522,96 @@
480 522 }
481 523 }
482 524
483 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 +/**
484 614 * Get the product tags.
485 615 */
486 616 if ( ! function_exists( 'merchant_get_product_tags' ) ) {
487 617 function merchant_get_product_tags() {
@@ -673,8 +803,40 @@
673 803 return $date_object->getTimestamp(); // Output the timestamp
674 804 }
675 805 }
676 806
807 +if ( ! function_exists( 'merchant_date_string_to_timestamp' ) ) {
808 + /**
809 + * Convert a date string to a Unix timestamp based on the specified format and timezone.
810 + *
811 + * This function signature matches Merchant Pro for compatibility.
812 + *
813 + * @param string $date The date string to convert.
814 + * @param string|null $timezone_offset The timezone offset or timezone string. Defaults to WordPress timezone.
815 + * @param string $format The format of the date string. Defaults to 'm-d-Y h:i A'.
816 + *
817 + * @return int The timestamp, or 0 on failure.
818 + */
819 + function merchant_date_string_to_timestamp( $date, $timezone_offset = null, $format = 'm-d-Y h:i A' ) {
820 + try {
821 + // If it's already numeric, return it as-is
822 + if ( is_numeric( $date ) ) {
823 + return (int) $date;
824 + }
825 +
826 + // Use WordPress timezone if not specified
827 + $timezone_offset = $timezone_offset ?? wp_timezone_string();
828 +
829 + // Create DateTime object from the specified format and timezone
830 + $date_object = DateTime::createFromFormat( $format, $date, new DateTimeZone( $timezone_offset ) );
831 +
832 + return $date_object ? $date_object->getTimestamp() : 0;
833 + } catch ( Exception $e ) {
834 + return 0;
835 + }
836 + }
837 +}
838 +
677 839 /**
678 840 * Parses a list of product IDs
679 841 * @return array
680 842 */
@@ -700,48 +862,59 @@
700 862 * @return bool
701 863 */
702 864 if ( ! function_exists( 'merchant_is_user_condition_passed' ) ) {
703 865 function merchant_is_user_condition_passed( $args = array() ) {
704 - $passed = false;
866 + $args = is_array( $args ) ? $args : array();
705 867
706 868 $is_logged_in = is_user_logged_in();
707 869 $current_user = $is_logged_in ? wp_get_current_user() : null;
870 + $customer_id = is_object( $current_user ) && isset( $current_user->ID ) ? (int) $current_user->ID : 0;
871 + $user_role = is_object( $current_user ) && isset( $current_user->roles ) && ! empty( $current_user->roles ) ? $current_user->roles[0] : '';
708 872
709 873 $condition = $args['user_condition'] ?? 'all';
710 874
875 + $is_exclusion_enabled = $args['user_exclusion_enabled'] ?? false;
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();
880 +
711 881 switch ( $condition ) {
712 882 case 'all':
713 883 case '':
714 - $passed = true;
715 - break;
884 + if ( $is_exclusion_enabled ) {
885 + if ( in_array( $customer_id, $excluded_customers, true ) ) {
886 + return false;
887 + }
716 888
889 + if ( $condition === 'all' && in_array( $user_role, $excluded_roles, true ) ) {
890 + return false;
891 + }
892 + }
893 +
894 + return true;
895 +
717 896 case 'logged-in':
718 - if ( $is_logged_in ) {
719 - $passed = true;
720 - }
721 - break;
897 + return $is_logged_in;
722 898
723 899 case 'roles':
724 - $roles = $args['user_condition_roles'] ?? array();
725 - $role = $current_user->roles[0] ?? '';
900 + if ( $is_exclusion_enabled && in_array( $customer_id, $excluded_customers, true ) ) {
901 + return false;
902 + }
726 903
727 - if ( in_array( $role, $roles, true ) ) {
728 - $passed = true;
729 - }
730 - break;
904 + $allowed_roles_raw = $args['user_condition_roles'] ?? array();
905 + $allowed_roles = is_array( $allowed_roles_raw ) ? $allowed_roles_raw : array();
906 + return in_array( $user_role, $allowed_roles, true );
731 907
732 908 case 'customers':
733 - $customers_id = $args['user_condition_users'] ?? array();
734 - $customers_id = array_map( 'intval', $customers_id );
735 - $customer_id = (int) ( $current_user->ID ?? 0 );
909 + case 'users':
910 + $allowed_users_raw = $args['user_condition_users'] ?? array();
911 + $allowed_customers = array_map( 'intval', is_array( $allowed_users_raw ) ? $allowed_users_raw : array() );
912 + return $is_logged_in && in_array( $customer_id, $allowed_customers, true );
736 913
737 - if ( in_array( $customer_id, $customers_id, true ) ) {
738 - $passed = true;
739 - }
740 - break;
914 + default:
915 + return false;
741 916 }
742 -
743 - return $passed;
744 917 }
745 918 }
746 919
747 920 /**
@@ -788,10 +961,10 @@
788 961 }
789 962 }
790 963
791 964 // Exclude categories
792 - if ( in_array( $display_rule, array( 'all', 'all_products' ), true ) ) {
793 - $excluded_categories_slugs = $args['excluded_categories'] ?? array();
965 + if ( in_array( $display_rule, array( 'all', 'all_products', 'categories', 'category', 'by_category' ), true ) ) {
966 + $excluded_categories_slugs = merchant_maybe_expand_categories( $args['excluded_categories'] ?? array(), $args );
794 967
795 968 if ( ! empty( $excluded_categories_slugs ) && has_term( $excluded_categories_slugs, 'product_cat', $_product_id ) ) {
796 969 return true;
797 970 }
@@ -797,9 +970,9 @@
797 970 }
798 971 }
799 972
800 973 // Exclude tags
801 - if ( in_array( $display_rule, array( 'all', 'all_products' ), true ) ) {
974 + if ( in_array( $display_rule, array( 'all', 'all_products', 'tags', 'by_tags' ), true ) ) {
802 975 $excluded_tags_slugs = $args['excluded_tags'] ?? array();
803 976
804 977 if ( ! empty( $excluded_tags_slugs ) && has_term( $excluded_tags_slugs, 'product_tag', $_product_id ) ) {
805 978 return true;
@@ -805,8 +978,44 @@
805 978 return true;
806 979 }
807 980 }
808 981
982 + // Exclude brands
983 + if ( in_array( $display_rule, array( 'all', 'all_products', 'brands', 'by_brands' ), true ) ) {
984 + $excluded_brands_slugs = $args['excluded_brands'] ?? array();
985 +
986 + if ( ! empty( $excluded_brands_slugs ) && has_term( $excluded_brands_slugs, 'product_brand', $_product_id ) ) {
987 + return true;
988 + }
989 + }
990 +
991 + // Exclude categories with toggle
992 + if ( ! empty( $args['exclude_categories_toggle'] ) && in_array( $display_rule, array( 'all', 'all_products', 'categories', 'category', 'by_category' ), true ) ) {
993 + $excluded_categories_slugs = merchant_maybe_expand_categories( $args['excluded_categories'] ?? array(), $args );
994 +
995 + if ( ! empty( $excluded_categories_slugs ) && has_term( $excluded_categories_slugs, 'product_cat', $_product_id ) ) {
996 + return true;
997 + }
998 + }
999 +
1000 + // Exclude tags with toggle
1001 + if ( ! empty( $args['exclude_tags_toggle'] ) && in_array( $display_rule, array( 'all', 'all_products', 'tags', 'by_tags' ), true ) ) {
1002 + $excluded_tags_slugs = $args['excluded_tags'] ?? array();
1003 +
1004 + if ( ! empty( $excluded_tags_slugs ) && has_term( $excluded_tags_slugs, 'product_tag', $_product_id ) ) {
1005 + return true;
1006 + }
1007 + }
1008 +
1009 + // Exclude brands with toggle
1010 + if ( ! empty( $args['exclude_brands_toggle'] ) && in_array( $display_rule, array( 'all', 'all_products', 'brands', 'by_brands' ), true ) ) {
1011 + $excluded_brands_slugs = $args['excluded_brands'] ?? array();
1012 +
1013 + if ( ! empty( $excluded_brands_slugs ) && has_term( $excluded_brands_slugs, 'product_brand', $_product_id ) ) {
1014 + return true;
1015 + }
1016 + }
1017 +
809 1018 return false;
810 1019 }
811 1020 }
812 1021
@@ -874,6 +1083,163 @@
874 1083 function merchant_get_product_reviews_count( $product_id ) {
875 1084 $product = wc_get_product( $product_id );
876 1085
877 1086 return $product ? $product->get_review_count() : 0;
1087 + }
1088 +}
1089 +
1090 +if ( ! function_exists( 'merchant_get_modules_campaign_data' ) ) {
1091 + /**
1092 + * Retrieves campaign data for a specific module.
1093 + *
1094 + * This function fetches campaign data based on the provided module ID and processes it
1095 + * into a standardized format. The campaigns are indexed by their `flexible_id` (expected to be a UUID v4)
1096 + * and include details such as ID, title, and status.
1097 + *
1098 + * @param string $module_id The ID of the module for which to retrieve campaign data.
1099 + * Expected values include constants like `Merchant_Buy_X_Get_Y::MODULE_ID`,
1100 + * `Merchant_Pre_Orders::MODULE_ID`, and `Merchant_Product_Labels::MODULE_ID`.
1101 + *
1102 + * @return array An associative array of campaigns, where each key is the `flexible_id` (UUID v4)
1103 + * of the campaign, and the value is an array containing the campaign's `id`, `title`,
1104 + * and `status`.
1105 + * Example:
1106 + * [
1107 + * '550e8400-e29b-41d4-a716-446655440000' => [
1108 + * 'id' => '550e8400-e29b-41d4-a716-446655440000',
1109 + * 'title' => 'Campaign Title',
1110 + * 'status' => 'active',
1111 + * ],
1112 + * 'f47ac10b-58cc-4372-a567-0e02b2c3d479' => [
1113 + * 'id' => 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
1114 + * 'title' => 'Another Campaign',
1115 + * 'status' => 'inactive',
1116 + * ],
1117 + * ]
1118 + */
1119 + function merchant_get_module_campaigns_data( $module_id ) {
1120 + $campaigns = array();
1121 +
1122 + // Determine the key to use based on the module ID
1123 + $key = '';
1124 + if ( Merchant_Buy_X_Get_Y::MODULE_ID === $module_id || Merchant_Pre_Orders::MODULE_ID === $module_id ) {
1125 + $key = 'rules';
1126 + } elseif ( Merchant_Product_Labels::MODULE_ID === $module_id ) {
1127 + $key = 'labels';
1128 + } else {
1129 + $key = 'offers';
1130 + }
1131 +
1132 + // Fetch the module campaigns
1133 + $module_campaigns = Merchant_Admin_Options::get( $module_id, $key, array() );
1134 + if ( empty( $module_campaigns ) ) {
1135 + return $campaigns;
1136 + }
1137 +
1138 + // Process each campaign
1139 + foreach ( $module_campaigns as $campaign ) {
1140 + if ( empty( $campaign['flexible_id'] ) ) {
1141 + continue;
1142 + }
1143 +
1144 + $campaigns[ $campaign['flexible_id'] ] = array(
1145 + 'campaign_key' => $key,
1146 + 'campaign_id' => $campaign['flexible_id'],
1147 + 'campaign_title' => $campaign['offer-title'] ?? $campaign['label-title'] ?? '',
1148 + 'campaign_status' => ( isset( $campaign['campaign_status'] ) && $campaign['campaign_status'] === 'inactive' ) ? 'inactive' : 'active',
1149 + );
1150 + }
1151 +
1152 + return $campaigns;
1153 + }
1154 +}
1155 +
1156 +if ( ! function_exists( 'merchant_get_modules_data' ) ) {
1157 + /**
1158 + * Get the data of all the modules.
1159 + *
1160 + * @return array
1161 + */
1162 + function merchant_get_modules_data() {
1163 + $modules = array();
1164 + $registered_modules = Merchant_Admin_Modules::get_modules();
1165 + foreach ( $registered_modules as $module_category => $module_data ) {
1166 + foreach ( $module_data['modules'] as $module_id => $module ) {
1167 + if ( ! empty( $module ) ) {
1168 + $modules[ $module_id ] = array(
1169 + 'id' => $module_id,
1170 + 'active' => Merchant_Modules::is_module_active( $module_id ),
1171 + 'name' => $module['title'],
1172 + 'desc' => $module['desc'],
1173 + 'category' => $module_category,
1174 + 'pro' => $module['pro'],
1175 + );
1176 +
1177 + $campaigns = merchant_get_module_campaigns_data( $module_id );
1178 +
1179 + if ( ! empty( $campaigns ) ) {
1180 + $modules[ $module_id ]['campaigns'] = $campaigns;
1181 + }
1182 + }
1183 + }
1184 + }
1185 +
1186 + return $modules;
1187 + }
1188 +}
1189 +
1190 +if ( ! function_exists( 'merchant_get_active_modules' ) ) {
1191 + /**
1192 + * Get the active modules.
1193 + *
1194 + * @return array
1195 + */
1196 + function merchant_get_active_modules() {
1197 + $modules = array_keys( merchant_get_modules_data() );
1198 +
1199 + return array_values( array_filter( $modules, static function ( $module_id ) {
1200 + return Merchant_Modules::is_module_active( $module_id );
1201 + } ) );
1202 + }
1203 +}
1204 +
1205 +if ( ! function_exists( 'merchant_get_campaign_data' ) ) {
1206 + /**
1207 + * Get the data of a specific campaign.
1208 + *
1209 + * @param string $campaign_id The ID of the campaign.
1210 + * @param string $module_id The ID of the module.
1211 + *
1212 + * @return array
1213 + */
1214 + function merchant_get_campaign_data( $campaign_id, $module_id ) {
1215 + if ( Merchant_Buy_X_Get_Y::MODULE_ID === $module_id || Merchant_Pre_Orders::MODULE_ID === $module_id ) {
1216 + $key = 'rules';
1217 + } elseif ( Merchant_Product_Labels::MODULE_ID === $module_id ) {
1218 + $key = 'labels';
1219 + } elseif ( 'buy-now' === $module_id ) {
1220 + $key = 'campaigns';
1221 + } else {
1222 + $key = 'offers';
1223 + }
1224 +
1225 + $all_modules = merchant_get_modules_data();
1226 + $module_campaigns = Merchant_Admin_Options::get( $module_id, $key, array() );
1227 + if ( ! empty( $module_campaigns ) ) {
1228 + foreach ( $module_campaigns as $campaign ) {
1229 + if ( isset( $all_modules[ $module_id ], $campaign['flexible_id'] ) && $campaign_id === $campaign['flexible_id'] ) {
1230 + return array(
1231 + 'campaign_id' => $campaign['flexible_id'],
1232 + 'campaign_title' => $campaign['offer-title'] ?? $campaign['label-title'] ?? '',
1233 + 'campaign_status' => $campaign['status'] ?? 'active',
1234 + 'module_id' => $module_id,
1235 + 'module_name' => $all_modules[ $module_id ]['name'],
1236 + 'is_pro' => $all_modules[ $module_id ]['pro'],
1237 + 'is_module_active' => $all_modules[ $module_id ]['active'],
1238 + );
1239 + }
1240 + }
1241 + }
1242 +
1243 + return array();
878 1244 }
879 1245 }