Plugin Directory

Changeset 3457682


Ignore:
Timestamp:
02/10/2026 06:37:54 AM (7 weeks ago)
Author:
assafadscale
Message:

Added tags and brands for products

Location:
adscale-ai/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • adscale-ai/trunk/adscale-ai.php

    r3437579 r3457682  
    88 * Plugin URI: https://www.adscale.com/integration/#woocommerce
    99 * Description: AdScale plugin allows you to automate Ecommerce advertising across all channels and drive more sales to your store. Easily create, manage & optimize ads on one platform.
    10  * Version: 2.2.14
     10 * Version: 2.2.16
    1111 * Author: AdScale LTD
    1212 * Author URI: https://www.adscale.com
     
    2323defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
    2424use AdScale\App;
    25 define( 'ADSCALE_INTERNAL_MODULE_VERSION', 'v20260112-M' );
     25define( 'ADSCALE_INTERNAL_MODULE_VERSION', 'v20260209-M' );
    2626define( 'ADSCALE_PLUGIN_DIR', __DIR__ );
    2727define( 'ADSCALE_PLUGIN_FILE', __FILE__ );
  • adscale-ai/trunk/changelog.txt

    r3437579 r3457682  
    11*** AdScale AI Changelog ***
     2
     32026-02-09 - version 2.1.16
     4* Fix: prevent API JSON responses from being corrupted by wp_kses sanitization.
     5* Enhancement: product API now returns tag names and brand names.
     6
     72026-01-26 - version 2.1.15
     8* Fix: stable HPOS pagination for orders (limit/after) using field_query cursor (id > after).
    29
    3102026-01-12 - version 2.2.14
  • adscale-ai/trunk/readme.txt

    r3437579 r3457682  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.2.14
     7Stable tag: 2.2.16
    88License: GPL-2.0+
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
  • adscale-ai/trunk/src/Helpers/Helper.php

    r3435129 r3457682  
    121121     */
    122122    public static function get_sanitize_description( $desc ) {
    123         $clean = wp_check_invalid_utf8( $desc ?? '' );
     123
     124        // Ensure scalar input (some editors/meta can return arrays/objects).
     125        if ( is_array( $desc ) || is_object( $desc ) ) {
     126            $desc = '';
     127        }
     128
     129        $clean = (string) ( $desc ?? '' );
     130
     131        // Try to normalize to UTF-8 (helps when descriptions contain mixed encodings).
     132        if ( function_exists( 'mb_detect_encoding' ) && function_exists( 'mb_convert_encoding' ) ) {
     133            // Some PHP/mbstring builds don't support certain aliases (e.g. Windows-1255) and can throw ValueError.
     134            $candidates = array( 'UTF-8', 'CP1255', 'ISO-8859-8', 'ISO-8859-1' );
     135
     136            if ( function_exists( 'mb_list_encodings' ) ) {
     137                $supported = array_map( 'strtoupper', (array) mb_list_encodings() );
     138                $candidates = array_values( array_filter( $candidates, static function ( $e ) use ( $supported ) {
     139                    return in_array( strtoupper( $e ), $supported, true );
     140                } ) );
     141            }
     142
     143            // If list is empty for some reason, fall back to UTF-8 only.
     144            if ( empty( $candidates ) ) {
     145                $candidates = array( 'UTF-8' );
     146            }
     147
     148            $enc = mb_detect_encoding( $clean, $candidates, true );
     149            if ( $enc && 'UTF-8' !== strtoupper( $enc ) ) {
     150                $clean = mb_convert_encoding( $clean, 'UTF-8', $enc );
     151            }
     152        }
     153
     154        // Strip/replace invalid UTF-8 sequences.
     155        $clean = wp_check_invalid_utf8( $clean );
     156        if ( false === $clean ) {
     157            $clean = '';
     158        }
     159
     160        // If description contains a meta description tag (escaped or raw), use its content ONLY
     161        // when the description is essentially just that meta tag (otherwise keep normal cleaning).
     162        $meta_src = html_entity_decode( $clean, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
     163        // Work on decoded HTML so `<meta ...>` (even if originally `&lt;meta`) can be detected/removed reliably.
     164        $clean = $meta_src;
     165
     166        if ( preg_match( '/<meta\\s+[^>]*name\\s*=\\s*(?:\"|\')description(?:\"|\')\\s+[^>]*content\\s*=\\s*(?:\"|\')([^\"\\\']*)(?:\"|\\\')/iu', $meta_src, $m ) ) {
     167            $meta_desc = trim( (string) $m[1] );
     168            $meta_desc = preg_replace( '/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/u', '', $meta_desc );
     169            $meta_desc = preg_replace( '/\\s+/', ' ', $meta_desc );
     170
     171            // Remove the meta tag(s) and see if anything meaningful remains.
     172            $without_meta = preg_replace( '/<meta\\b[^>]*>/iu', ' ', $meta_src );
     173            $without_meta = wp_strip_all_tags( $without_meta );
     174            $without_meta = html_entity_decode( (string) $without_meta, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
     175            $without_meta = preg_replace( '/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]/u', '', $without_meta );
     176            $without_meta = preg_replace( '/\\s+/', ' ', $without_meta );
     177            $without_meta = trim( $without_meta );
     178
     179            // If there is no other content besides the meta tag, return meta content.
     180            if ( $without_meta === '' ) {
     181                return trim( $meta_desc );
     182            }
     183            // Remove the meta fragment even if it's malformed (missing '>') by stripping up to the end of the content attribute.
     184            $clean = preg_replace(
     185                '/<meta\\s+[^>]*name\\s*=\\s*(?:\"|\')description(?:\"|\')\\s+[^>]*content\\s*=\\s*(?:\"|\')[^\"\\\']*(?:\"|\\\')/iu',
     186                ' ' . $meta_desc . ' ',
     187                $clean,
     188                1
     189            );
     190            // Otherwise continue with normal sanitization (do not early return).
     191        }
     192
    124193        // Remove embedded media blocks (video/audio/images) from rich editors like Elementor.
    125194        // Strip full blocks first to avoid leaving media URLs in the resulting plain text.
     
    129198        $clean = preg_replace( '/<source\\b[^>]*>/is', ' ', $clean );
    130199        $clean = preg_replace( '/<img\\b[^>]*>/is', ' ', $clean );
     200        $clean = preg_replace( '/<meta\\b[^>]*>/is', ' ', $clean );
     201        $clean = preg_replace( '/<(noscript|script)\\b[^>]*>.*?<\\/(noscript|script)>/is', ' ', $clean );
    131202        // Remove anchor links entirely (href + linked text), so URLs don't leak into plain text.
    132203        $clean = preg_replace( '/<a\b[^>]*>.*?<\/a>/is', ' ', $clean );
     
    134205        $clean = wp_strip_all_tags( $clean );
    135206        // Decode HTML entities to plain text
    136         $clean = html_entity_decode( $clean, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
     207        $clean = html_entity_decode( (string) $clean, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
    137208        // Then remove the marker when it precedes a number.
    138209        $clean = preg_replace( '/[<<]\s*(?=\d)/u', '', $clean );
     
    149220        // Collapse whitespace.
    150221        $clean = preg_replace( '/\s+/', ' ', $clean );
    151         return trim( $clean );
     222        $clean = trim( $clean );
     223
     224        // Final guard: ensure the string can be safely JSON-encoded (no invalid UTF-8/control chars).
     225        // If encoding fails, remove problematic bytes and retry.
     226        if ( false === json_encode( array( 'd' => $clean ) ) ) {
     227            // Remove any remaining ASCII control chars.
     228            $clean = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $clean );
     229
     230            // Drop invalid UTF-8 sequences (WP helper).
     231            $tmp = wp_check_invalid_utf8( $clean );
     232            if ( false !== $tmp ) {
     233                $clean = $tmp;
     234            } else {
     235                $clean = '';
     236            }
     237
     238            // As a last resort, ignore invalid bytes via iconv if available.
     239            if ( $clean !== '' && function_exists( 'iconv' ) ) {
     240                $iconv = @iconv( 'UTF-8', 'UTF-8//IGNORE', $clean );
     241                if ( is_string( $iconv ) ) {
     242                    $clean = $iconv;
     243                }
     244            }
     245
     246            // Re-collapse whitespace after cleanup.
     247            $clean = preg_replace( '/\s+/', ' ', (string) $clean );
     248            $clean = trim( $clean );
     249        }
     250
     251
     252        return $clean;
    152253    }
    153254
  • adscale-ai/trunk/src/PluginApi/Orders.php

    r3437579 r3457682  
    304304     * @return \WC_Order[]
    305305     */
    306     private static function get_orders_via_wc_query()
    307     {
    308         $limit = isset(self::$requestParams['limit']) ? (int)self::$requestParams['limit'] : (int)self::$defaultLimit;
    309         $after = isset(self::$requestParams['after']) ? (int)self::$requestParams['after'] : 0;
    310 
    311         // When paginating by "after" (ID > after) under HPOS, we may need to over-fetch and filter in PHP.
    312         $query_limit = $limit;
    313         if ($after > 0) {
    314             $query_limit = $limit + min(200, $limit);
    315         }
     306    private static function get_orders_via_wc_query() {
     307        $limit = isset(self::$requestParams['limit']) ? (int) self::$requestParams['limit'] : (int) self::$defaultLimit;
     308        $after = isset(self::$requestParams['after']) ? (int) self::$requestParams['after'] : 0;
    316309
    317310        $args = array(
    318             'limit' => $query_limit,
     311            'limit'   => $limit,
    319312            'orderby' => 'id',
    320             'order' => 'ASC',
    321             'return' => 'objects',
    322             'type' => 'shop_order',
    323             'status' => 'any',
     313            'order'   => 'ASC',
     314            'return'  => 'objects',
     315            'type'    => 'shop_order',
     316            'status'  => 'any',
    324317        );
    325318
    326         // If specific IDs are requested, fetch them directly (works for both HPOS and legacy storage).
    327         if (!empty(self::$requestParams['ids'])) {
    328             $ids = array_map('absint', (array)self::$requestParams['ids']);
    329             $orders = array();
    330 
    331 
    332             foreach ($ids as $id) {
    333                 if (!$id) {
    334                     continue;
    335                 }
    336 
    337                 $order = wc_get_order($id);
    338                 if ($order instanceof \WC_Order) {
    339                     $orders[] = $order;
    340                 }
    341             }
    342             return $orders;
    343         }
    344 
    345         // Apply created date range (start/end) using WooCommerce string-based date filters.
     319        //  Cursor pagination in HPOS: ID > after
     320        if ( $after > 0 ) {
     321            $args['field_query'] = array(
     322                array(
     323                    'field'   => 'id',
     324                    'value'   => $after,
     325                    'compare' => '>',
     326                ),
     327            );
     328        }
     329
     330        // date_created (your current string logic is OK)
    346331        $start = !empty(self::$requestParams['start']) ? Helper::reformat_date(self::$requestParams['start'], 'dmY', 'Y-m-d') : '';
    347332        $end = !empty(self::$requestParams['end']) ? Helper::reformat_date(self::$requestParams['end'], 'dmY', 'Y-m-d') : '';
     
    354339        }
    355340
    356         // Apply modified-after filter using WooCommerce string-based date filter.
    357341        $changed_after = !empty(self::$requestParams['changed_after']) ? Helper::reformat_date(self::$requestParams['changed_after'], 'dmY', 'Y-m-d') : '';
    358342        if ($changed_after) {
     
    360344        }
    361345
     346        //  No overfetch, no PHP filtering, no array_slice needed
    362347        $orders = wc_get_orders($args);
    363         $orders = is_array($orders) ? $orders : array();
    364 
    365         // Apply "after" (ID > after) in PHP for HPOS to avoid relying on internal SQL filters.
    366         if ($after > 0) {
    367             $orders = array_values(
    368                 array_filter(
    369                     $orders,
    370                     function ($order) use ($after) {
    371                         return ($order instanceof \WC_Order) && ((int)$order->get_id() > $after);
    372                     }
    373                 )
    374             );
    375 
    376             $orders = array_slice($orders, 0, $limit);
    377         }
    378 
    379         return $orders;
     348
     349        return is_array($orders) ? $orders : array();
    380350    }
    381351
  • adscale-ai/trunk/src/PluginApi/Products.php

    r3429458 r3457682  
    277277           
    278278            $product_id = (int) $product->get_id();
    279            
     279
     280            // Get tag names
     281            $tag_names = wp_get_post_terms( $product_id, 'product_tag', [ 'fields' => 'names' ] );
     282
     283            // Get brand names (support common brand taxonomies)
     284            $brand_names = [];
     285
     286            $brand_taxonomies = [ 'product_brand', 'brand', 'brands' ];
     287            foreach ( $brand_taxonomies as $tax ) {
     288                if ( taxonomy_exists( $tax ) ) {
     289                    $terms = wp_get_post_terms( $product_id, $tax, [ 'fields' => 'names' ] );
     290                    if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
     291                        $brand_names = array_values( (array) $terms );
     292                        break;
     293                    }
     294                }
     295            }
     296
    280297            $variations_data = [];
    281            
     298
    282299            $variations_objs = wc_get_products(
    283300                [
     
    292309                ]
    293310            );
    294            
     311
    295312            $variations_objs_filtered = [];
    296313            if ( $variations_objs && is_array( $variations_objs ) ) {
    297314                foreach ( $variations_objs as $variation ) {
    298                    
     315
    299316                    // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked.
    300317                    if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
     
    309326                        continue;
    310327                    }
    311                    
     328
    312329                    $variations_objs_filtered[] = $variation;
    313330                }
    314331            }
    315            
    316            
     332
     333
    317334            $variants_count_real = count( $variations_objs_filtered );
    318335            $variants_count_real = $variants_count_real ? $variants_count_real : 1;
    319            
     336
    320337            if ( $variants_count_real > self::$requestParams['variants_limit'] ) {
    321338                $max_variants_num = self::$requestParams['variants_limit'];
    322339                $cheapest_num     = (int) floor( $max_variants_num / 2 );
    323340                $expensive_num    = $max_variants_num - $cheapest_num;
    324                
     341
    325342                $variations_objs_ordered = wc_products_array_orderby( $variations_objs_filtered, 'price', 'ASC' );
    326                
     343
    327344                $variations_objs_cheapest  = array_slice( $variations_objs_ordered, 0, $cheapest_num );
    328345                $variations_objs_expensive = array_slice( $variations_objs_ordered, - $expensive_num );
    329346                $variations_objs_resolved  = array_merge( $variations_objs_cheapest, $variations_objs_expensive );
    330                
     347
    331348            } else {
    332349                $variations_objs_resolved = $variations_objs_filtered;
    333350            }
    334            
    335            
     351
     352
    336353            if ( $variations_objs_resolved ) {
    337354                foreach ( $variations_objs_resolved as $variation ) {
     
    351368                'keywords'            => null,
    352369                'categories'          => array_values( (array) $product->get_category_ids() ),
     370                'tags'                => array_values( (array) $tag_names ),
     371                'brands'              => $brand_names,
    353372                'attributes'          => self::getAttr($product),
    354373                'variants_count_real' => $variants_count_real,
Note: See TracChangeset for help on using the changeset viewer.