get_meta( $key, true ); if ( is_scalar( $raw ) ) { $value = trim( wp_strip_all_tags( (string) $raw ) ); if ( $value !== '' ) { return trim( (string) apply_filters( 'tdl_gmc_product_brand', $value, $product, $variation ) ); } } } } // WooCommerce Brands uses the product_brand taxonomy in current WooCommerce versions. if ( taxonomy_exists( 'product_brand' ) ) { $terms = get_the_terms( $product->get_id(), 'product_brand' ); if ( is_array( $terms ) && ! empty( $terms ) ) { foreach ( $terms as $term ) { if ( isset( $term->name ) ) { $value = trim( wp_strip_all_tags( (string) $term->name ) ); if ( $value !== '' ) { return trim( (string) apply_filters( 'tdl_gmc_product_brand', $value, $product, $variation ) ); } } } } } $attribute_keys = [ 'pa_brand', 'brand', 'pa_manufacturer', 'manufacturer', 'pa_znacka', 'znacka', 'pa_vyrobce', 'vyrobce', ]; foreach ( $subjects as $subject ) { foreach ( $attribute_keys as $key ) { $value = trim( wp_strip_all_tags( (string) $subject->get_attribute( $key ) ) ); if ( $value !== '' ) { $value = trim( (string) explode( ',', $value )[0] ); return trim( (string) apply_filters( 'tdl_gmc_product_brand', $value, $product, $variation ) ); } } } return trim( (string) apply_filters( 'tdl_gmc_product_brand', resolve_brand(), $product, $variation ) ); } function get_option_gtin_keys(): array { return _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_gtin_keys', '' ) ); } function get_option_mpn_keys(): array { return _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_mpn_keys', '' ) ); } /** Validate a GTIN-8/12/13/14 including the GS1 check digit and Google-restricted prefixes. */ function _tdl_gmc_is_valid_gtin( string $value ): bool { $length = strlen( $value ); if ( ! in_array( $length, [ 8, 12, 13, 14 ], true ) || ! ctype_digit( $value ) ) { return false; } // Google Merchant Center does not accept restricted/internal-use or coupon GTIN ranges. if ( strpos( $value, '02' ) === 0 || strpos( $value, '04' ) === 0 || strpos( $value, '2' ) === 0 || strpos( $value, '98' ) === 0 || strpos( $value, '99' ) === 0 ) { return false; } $sum = 0; $weight = 3; for ( $i = $length - 2; $i >= 0; $i-- ) { $sum += (int) $value[ $i ] * $weight; $weight = ( $weight === 3 ) ? 1 : 3; } $check_digit = ( 10 - ( $sum % 10 ) ) % 10; return $check_digit === (int) $value[ $length - 1 ]; } /** Normalize a possible GTIN and return it only when it is valid for Google Merchant Center. */ function _tdl_gmc_norm_gtin( $value ): string { if ( is_array( $value ) ) { if ( isset( $value['gtin'] ) && is_scalar( $value['gtin'] ) ) { $value = $value['gtin']; } elseif ( isset( $value['identifier_gtin'] ) && is_scalar( $value['identifier_gtin'] ) ) { $value = $value['identifier_gtin']; } else { $value = ''; } } $value = is_scalar( $value ) ? trim( (string) $value ) : ''; if ( $value === '' ) { return ''; } if ( $value[0] === '{' || $value[0] === '[' ) { $decoded = json_decode( $value, true ); if ( is_array( $decoded ) ) { if ( isset( $decoded['gtin'] ) ) { $value = (string) $decoded['gtin']; } elseif ( isset( $decoded['identifier_gtin'] ) ) { $value = (string) $decoded['identifier_gtin']; } } } $value = (string) preg_replace( '/[^0-9]/', '', $value ); return _tdl_gmc_is_valid_gtin( $value ) ? $value : ''; } /** Variation-first GTIN lookup, then parent product. */ function find_gtin( WC_Product $product, ?WC_Product_Variation $variation = null ): string { $subjects = []; if ( $variation instanceof WC_Product_Variation ) { $subjects[] = $variation; } elseif ( $product instanceof WC_Product_Variation ) { $subjects[] = $product; $parent = wc_get_product( $product->get_parent_id() ); if ( $parent instanceof WC_Product ) { $subjects[] = $parent; } } if ( ! in_array( $product, $subjects, true ) ) { $subjects[] = $product; } foreach ( $subjects as $subject ) { if ( method_exists( $subject, 'get_global_unique_id' ) ) { $value = _tdl_gmc_norm_gtin( $subject->get_global_unique_id() ); if ( $value !== '' ) { return (string) apply_filters( 'tdl_gmc_gtin_value', $value, $subject ); } } } $default_keys = [ '_wc_gpf_gtin', '_woocommerce_gpf_gtin', '_woocommerce_gpf_data', '_wpm_gtin_code', '_alg_ean', '_alg_wc_ean', '_gtin', 'gtin', '_ean', 'ean', 'product_ean', '_product_ean', 'woodmart_barcode', 'woodmart_ean', '_woodmart_barcode', '_barcode', 'barcode', 'hwp_product_gtin', '_yith_wcgpf_gtin', '_yoast_wpseo_product_gtin', 'global_unique_id', 'variable_global_unique_id', ]; $keys = array_values( array_unique( array_merge( $default_keys, get_option_gtin_keys() ) ) ); $keys = (array) apply_filters( 'tdl_gmc_gtin_keys', $keys, $product, $variation ); foreach ( $subjects as $subject ) { foreach ( $keys as $key ) { $key = trim( (string) $key ); if ( $key === '' ) { continue; } $raw = $subject->get_meta( $key, true ); $value = _tdl_gmc_norm_gtin( $raw ); if ( $value !== '' ) { return (string) apply_filters( 'tdl_gmc_gtin_value', $value, $subject ); } if ( $key === '_woocommerce_gpf_data' ) { $maybe = is_string( $raw ) ? maybe_unserialize( $raw ) : $raw; if ( is_array( $maybe ) && isset( $maybe['gtin'] ) ) { $value = _tdl_gmc_norm_gtin( $maybe['gtin'] ); if ( $value !== '' ) { return (string) apply_filters( 'tdl_gmc_gtin_value', $value, $subject ); } } } } $attrs = $subject->get_attributes(); if ( is_array( $attrs ) ) { foreach ( $attrs as $attr ) { if ( ! $attr instanceof WC_Product_Attribute ) { continue; } $name = $attr->get_name(); $name_norm = strtolower( sanitize_title( $name ) ); if ( strpos( $name_norm, 'ean' ) === false && strpos( $name_norm, 'gtin' ) === false ) { continue; } $value = _tdl_gmc_norm_gtin( $subject->get_attribute( $name ) ); if ( $value !== '' ) { return (string) apply_filters( 'tdl_gmc_gtin_value', $value, $subject ); } } } } return (string) apply_filters( 'tdl_gmc_gtin_value', '', $product ); } /** Variation-first MPN lookup. SKU is used only when explicitly enabled. */ function find_mpn( WC_Product $product, ?WC_Product_Variation $variation = null ): string { $subjects = []; if ( $variation instanceof WC_Product_Variation ) { $subjects[] = $variation; } elseif ( $product instanceof WC_Product_Variation ) { $subjects[] = $product; $parent = wc_get_product( $product->get_parent_id() ); if ( $parent instanceof WC_Product ) { $subjects[] = $parent; } } if ( ! in_array( $product, $subjects, true ) ) { $subjects[] = $product; } $default_keys = [ '_mpn', 'mpn', '_manufacturer_part_number', 'manufacturer_part_number', '_wc_gpf_mpn', '_woocommerce_gpf_mpn', '_wpm_mpn', '_yith_wcgpf_mpn', ]; $keys = array_values( array_unique( array_merge( $default_keys, get_option_mpn_keys() ) ) ); $keys = (array) apply_filters( 'tdl_gmc_mpn_keys', $keys, $product, $variation ); foreach ( $subjects as $subject ) { foreach ( $keys as $key ) { $raw = $subject->get_meta( (string) $key, true ); if ( is_scalar( $raw ) ) { $value = trim( wp_strip_all_tags( (string) $raw ) ); if ( $value !== '' ) { return limit_text( (string) apply_filters( 'tdl_gmc_mpn_value', $value, $subject ), 70 ); } } } $attrs = $subject->get_attributes(); if ( is_array( $attrs ) ) { foreach ( $attrs as $attr ) { if ( ! $attr instanceof WC_Product_Attribute ) { continue; } $name = strtolower( sanitize_title( $attr->get_name() ) ); if ( strpos( $name, 'mpn' ) === false && strpos( $name, 'manufacturer-part-number' ) === false ) { continue; } $value = trim( wp_strip_all_tags( (string) $subject->get_attribute( $attr->get_name() ) ) ); if ( $value !== '' ) { return limit_text( (string) apply_filters( 'tdl_gmc_mpn_value', $value, $subject ), 70 ); } } } } $use_sku = (bool) get_option( 'tdl_gmc_use_sku_as_mpn', false ); $use_sku = (bool) apply_filters( 'tdl_gmc_use_sku_as_mpn', $use_sku, $product, $variation ); if ( $use_sku ) { foreach ( $subjects as $subject ) { $sku = trim( (string) $subject->get_sku() ); if ( $sku !== '' ) { return limit_text( (string) apply_filters( 'tdl_gmc_mpn_value', $sku, $subject ), 70 ); } } } return (string) apply_filters( 'tdl_gmc_mpn_value', '', $variation ?: $product ); } /** Return [identifier_exists, gtin, mpn, brand]. */ function identifier_block( WC_Product $product, ?WC_Product_Variation $variation = null ): array { $gtin = find_gtin( $product, $variation ); $mpn = find_mpn( $product, $variation ); $brand = resolve_product_brand( $product, $variation ); $identifier_exists = ( $gtin !== '' || ( $mpn !== '' && $brand !== '' ) ) ? 'yes' : 'no'; return [ $identifier_exists, $gtin, $mpn, $brand ]; } function _tdl_gmc_format_number( float $number ): string { $formatted = number_format( $number, 6, '.', '' ); $formatted = rtrim( rtrim( $formatted, '0' ), '.' ); return $formatted === '' ? '0' : $formatted; } function _tdl_gmc_normalize_measure_unit( string $unit ): string { $unit = strtolower( trim( $unit ) ); $map = [ 'gram' => 'g', 'grams' => 'g', 'kg' => 'kg', 'kilogram' => 'kg', 'kilograms' => 'kg', 'mg' => 'mg', 'milligram' => 'mg', 'milligrams' => 'mg', 'oz' => 'oz', 'ounce' => 'oz', 'ounces' => 'oz', 'lb' => 'lb', 'lbs' => 'lb', 'pound' => 'lb', 'pounds' => 'lb', 'ml' => 'ml', 'milliliter' => 'ml', 'millilitre' => 'ml', 'milliliters' => 'ml', 'millilitres' => 'ml', 'cl' => 'cl', 'centiliter' => 'cl', 'centilitre' => 'cl', 'l' => 'l', 'lt' => 'l', 'liter' => 'l', 'litre' => 'l', 'liters' => 'l', 'litres' => 'l', ]; return $map[ $unit ] ?? $unit; } function _tdl_gmc_measure_base_for_unit( string $unit ): string { $bases = [ 'mg' => '100mg', 'g' => '100g', 'kg' => '1kg', 'oz' => '1oz', 'lb' => '1lb', 'ml' => '100ml', 'cl' => '1cl', 'l' => '1l', ]; return $bases[ $unit ] ?? ''; } function derive_unit_measures( WC_Product $product, ?WC_Product_Variation $variation = null ): array { $value = ''; $attr_keys = [ 'pa_weight', 'weight', 'pa_hmotnost', 'hmotnost', 'pa_vyberte-hmotnost', 'vyberte-hmotnost', 'pa_volume', 'volume', 'pa_objem', 'objem', ]; foreach ( $attr_keys as $key ) { if ( $variation instanceof WC_Product_Variation ) { $value = trim( (string) $variation->get_attribute( $key ) ); } if ( $value === '' ) { $value = trim( (string) $product->get_attribute( $key ) ); } if ( $value !== '' ) { break; } } // Do not fall back to WooCommerce's generic product weight. That field is often // used for shipping/logistics, while Google's unit_pricing_measure must describe // the quantity of product being sold (for example 100 g of tea or 750 ml of oil). // Unit pricing is therefore emitted only from an explicit weight/volume attribute. if ( ! preg_match( '/([0-9]+(?:[\.,][0-9]+)?)\s*([a-zA-Z]+)/u', $value, $matches ) ) { return [ '', '' ]; } $number = (float) str_replace( ',', '.', $matches[1] ); $unit = _tdl_gmc_normalize_measure_unit( $matches[2] ); $base = _tdl_gmc_measure_base_for_unit( $unit ); if ( $number <= 0 || $base === '' ) { return [ '', '' ]; } return [ _tdl_gmc_format_number( $number ) . $unit, $base ]; } function product_variant_url( WC_Product $product, ?WC_Product_Variation $variation = null ): string { $link = $product->get_permalink(); if ( $variation instanceof WC_Product_Variation ) { $attrs = $variation->get_attributes(); if ( is_array( $attrs ) && $attrs ) { $query = []; foreach ( $attrs as $taxonomy => $value ) { if ( $value === '' ) { continue; } $key = strpos( $taxonomy, 'attribute_' ) === 0 ? $taxonomy : 'attribute_' . ( strpos( $taxonomy, 'pa_' ) === 0 ? $taxonomy : sanitize_title( $taxonomy ) ); $query[ $key ] = (string) $value; } $parts = wp_parse_url( $link ); if ( is_array( $parts ) ) { $existing = []; if ( ! empty( $parts['query'] ) ) { parse_str( (string) $parts['query'], $existing ); } $parts['query'] = http_build_query( array_merge( $existing, $query ), '', '&', PHP_QUERY_RFC3986 ); $link = _tdl_gmc_build_url_from_parts( $parts ); } else { $link = add_query_arg( $query, $link ); } } } return (string) apply_filters( 'tdl_gmc_product_url', esc_url_raw( $link ), $product, $variation ); } function _tdl_gmc_build_url_from_parts( array $parts ): string { $url = ''; if ( isset( $parts['scheme'] ) ) $url .= $parts['scheme'] . '://'; if ( isset( $parts['user'] ) ) { $url .= $parts['user']; if ( isset( $parts['pass'] ) ) $url .= ':' . $parts['pass']; $url .= '@'; } if ( isset( $parts['host'] ) ) $url .= $parts['host']; if ( isset( $parts['port'] ) ) $url .= ':' . $parts['port']; if ( isset( $parts['path'] ) ) { $segments = explode( '/', (string) $parts['path'] ); $segments = array_map( static function ( string $segment ): string { return rawurlencode( rawurldecode( $segment ) ); }, $segments ); $url .= implode( '/', $segments ); } if ( ! empty( $parts['query'] ) ) $url .= '?' . $parts['query']; if ( ! empty( $parts['fragment'] ) ) $url .= '#' . rawurlencode( rawurldecode( (string) $parts['fragment'] ) ); return $url; } function _tdl_gmc_parse_list_option( string $option ): array { $parts = preg_split( '/[\s,\n\r;]+/', $option ); if ( ! is_array( $parts ) ) return []; $out = []; foreach ( $parts as $part ) { $part = trim( (string) $part ); if ( $part !== '' ) $out[] = $part; } return array_values( array_unique( $out ) ); } function _tdl_gmc_parse_measure_to_base( string $raw ): array { $raw = trim( $raw ); if ( $raw === '' || ! preg_match( '/([0-9]+(?:[\.,][0-9]+)?)\s*([a-zA-Z]+)/u', $raw, $matches ) ) { return [ '', 0.0 ]; } $number = (float) str_replace( ',', '.', $matches[1] ); $unit = _tdl_gmc_normalize_measure_unit( $matches[2] ); switch ( $unit ) { case 'mg': return [ 'mg', $number ]; case 'g': return [ 'mg', $number * 1000.0 ]; case 'kg': return [ 'mg', $number * 1000000.0 ]; case 'oz': return [ 'oz', $number ]; case 'lb': return [ 'oz', $number * 16.0 ]; case 'ml': return [ 'ml', $number ]; case 'cl': return [ 'ml', $number * 10.0 ]; case 'l': return [ 'ml', $number * 1000.0 ]; default: return [ '', 0.0 ]; } } function _tdl_gmc_subject_measure_base( WC_Product $product, ?WC_Product_Variation $variation = null ): array { [ $measure ] = derive_unit_measures( $product, $variation ); return $measure === '' ? [ '', 0.0 ] : _tdl_gmc_parse_measure_to_base( $measure ); } function tdl_gmc_filter_result( WC_Product $product, ?WC_Product_Variation $variation = null ): array { $subject = $variation ?: $product; if ( (bool) get_option( 'tdl_gmc_exclude_out_of_stock', false ) && ! $subject->is_in_stock() ) { return [ 'pass' => false, 'reason' => 'out_of_stock' ]; } $price_raw = wc_get_price_including_tax( $subject ); $price = is_numeric( $price_raw ) ? (float) $price_raw : (float) $subject->get_price(); if ( (bool) get_option( 'tdl_gmc_exclude_zero_price', true ) && $price <= 0 ) { return [ 'pass' => false, 'reason' => 'zero_price' ]; } $sku_list = _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_filter_skus', '' ) ); if ( $sku_list ) { $sku = strtolower( trim( (string) $subject->get_sku() ) ); $allowed = array_map( static function ( $value ): string { return strtolower( trim( (string) $value ) ); }, $sku_list ); if ( $sku === '' || ! in_array( $sku, $allowed, true ) ) { return [ 'pass' => false, 'reason' => 'sku_filter' ]; } } $include = _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_filter_cat_include', '' ) ); $exclude = _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_filter_cat_exclude', '' ) ); if ( $include || $exclude ) { $terms = get_the_terms( $product->get_id(), 'product_cat' ); $term_ids = []; $term_slugs = []; if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $term_ids[] = (int) $term->term_id; if ( isset( $term->slug ) ) $term_slugs[] = sanitize_title( (string) $term->slug ); $ancestors = get_ancestors( (int) $term->term_id, 'product_cat' ); if ( is_array( $ancestors ) ) { foreach ( $ancestors as $ancestor_id ) { $term_ids[] = (int) $ancestor_id; $ancestor = get_term( (int) $ancestor_id, 'product_cat' ); if ( $ancestor && ! is_wp_error( $ancestor ) && isset( $ancestor->slug ) ) { $term_slugs[] = sanitize_title( (string) $ancestor->slug ); } } } } } $term_ids = array_values( array_unique( $term_ids ) ); $term_slugs = array_values( array_unique( $term_slugs ) ); $matches_any = static function ( array $needles ) use ( $term_ids, $term_slugs ): bool { foreach ( $needles as $needle ) { $needle = trim( (string) $needle ); if ( $needle === '' ) continue; if ( ctype_digit( $needle ) && in_array( (int) $needle, $term_ids, true ) ) return true; if ( in_array( sanitize_title( $needle ), $term_slugs, true ) ) return true; } return false; }; if ( $include && ! $matches_any( $include ) ) { return [ 'pass' => false, 'reason' => 'category_filter' ]; } if ( $exclude && $matches_any( $exclude ) ) { return [ 'pass' => false, 'reason' => 'category_filter' ]; } } $min_raw = trim( (string) get_option( 'tdl_gmc_filter_price_min', '' ) ); $max_raw = trim( (string) get_option( 'tdl_gmc_filter_price_max', '' ) ); $min = $min_raw === '' ? null : (float) str_replace( ',', '.', $min_raw ); $max = $max_raw === '' ? null : (float) str_replace( ',', '.', $max_raw ); if ( $min !== null && $price < $min ) return [ 'pass' => false, 'reason' => 'price_filter' ]; if ( $max !== null && $price > $max ) return [ 'pass' => false, 'reason' => 'price_filter' ]; $weights = _tdl_gmc_parse_list_option( (string) get_option( 'tdl_gmc_filter_weights', '' ) ); if ( $weights ) { [ $unit, $value ] = _tdl_gmc_subject_measure_base( $product, $variation ); if ( $unit === '' ) return [ 'pass' => false, 'reason' => 'measure_filter' ]; $allowed = false; foreach ( $weights as $weight ) { [ $required_unit, $required_value ] = _tdl_gmc_parse_measure_to_base( (string) $weight ); if ( $required_unit === $unit && abs( $required_value - $value ) < 0.0001 ) { $allowed = true; break; } } if ( ! $allowed ) return [ 'pass' => false, 'reason' => 'measure_filter' ]; } $pass = (bool) apply_filters( 'tdl_gmc_passes_filters', true, $product, $variation ); return [ 'pass' => $pass, 'reason' => $pass ? '' : 'custom_filter' ]; } function tdl_gmc_passes_filters( WC_Product $product, ?WC_Product_Variation $variation = null ): bool { $result = tdl_gmc_filter_result( $product, $variation ); return ! empty( $result['pass'] ); }