Changeset 3457682
- Timestamp:
- 02/10/2026 06:37:54 AM (7 weeks ago)
- Location:
- adscale-ai/trunk
- Files:
-
- 6 edited
-
adscale-ai.php (modified) (2 diffs)
-
changelog.txt (modified) (1 diff)
-
readme.txt (modified) (1 diff)
-
src/Helpers/Helper.php (modified) (4 diffs)
-
src/PluginApi/Orders.php (modified) (3 diffs)
-
src/PluginApi/Products.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
adscale-ai/trunk/adscale-ai.php
r3437579 r3457682 8 8 * Plugin URI: https://www.adscale.com/integration/#woocommerce 9 9 * 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.1 410 * Version: 2.2.16 11 11 * Author: AdScale LTD 12 12 * Author URI: https://www.adscale.com … … 23 23 defined( 'ABSPATH' ) || exit; // Exit if accessed directly. 24 24 use AdScale\App; 25 define( 'ADSCALE_INTERNAL_MODULE_VERSION', 'v20260 112-M' );25 define( 'ADSCALE_INTERNAL_MODULE_VERSION', 'v20260209-M' ); 26 26 define( 'ADSCALE_PLUGIN_DIR', __DIR__ ); 27 27 define( 'ADSCALE_PLUGIN_FILE', __FILE__ ); -
adscale-ai/trunk/changelog.txt
r3437579 r3457682 1 1 *** AdScale AI Changelog *** 2 3 2026-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 7 2026-01-26 - version 2.1.15 8 * Fix: stable HPOS pagination for orders (limit/after) using field_query cursor (id > after). 2 9 3 10 2026-01-12 - version 2.2.14 -
adscale-ai/trunk/readme.txt
r3437579 r3457682 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2.2.1 47 Stable tag: 2.2.16 8 8 License: GPL-2.0+ 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html -
adscale-ai/trunk/src/Helpers/Helper.php
r3435129 r3457682 121 121 */ 122 122 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 `<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 124 193 // Remove embedded media blocks (video/audio/images) from rich editors like Elementor. 125 194 // Strip full blocks first to avoid leaving media URLs in the resulting plain text. … … 129 198 $clean = preg_replace( '/<source\\b[^>]*>/is', ' ', $clean ); 130 199 $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 ); 131 202 // Remove anchor links entirely (href + linked text), so URLs don't leak into plain text. 132 203 $clean = preg_replace( '/<a\b[^>]*>.*?<\/a>/is', ' ', $clean ); … … 134 205 $clean = wp_strip_all_tags( $clean ); 135 206 // 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' ); 137 208 // Then remove the marker when it precedes a number. 138 209 $clean = preg_replace( '/[<<]\s*(?=\d)/u', '', $clean ); … … 149 220 // Collapse whitespace. 150 221 $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; 152 253 } 153 254 -
adscale-ai/trunk/src/PluginApi/Orders.php
r3437579 r3457682 304 304 * @return \WC_Order[] 305 305 */ 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; 316 309 317 310 $args = array( 318 'limit' => $query_limit,311 'limit' => $limit, 319 312 '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', 324 317 ); 325 318 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) 346 331 $start = !empty(self::$requestParams['start']) ? Helper::reformat_date(self::$requestParams['start'], 'dmY', 'Y-m-d') : ''; 347 332 $end = !empty(self::$requestParams['end']) ? Helper::reformat_date(self::$requestParams['end'], 'dmY', 'Y-m-d') : ''; … … 354 339 } 355 340 356 // Apply modified-after filter using WooCommerce string-based date filter.357 341 $changed_after = !empty(self::$requestParams['changed_after']) ? Helper::reformat_date(self::$requestParams['changed_after'], 'dmY', 'Y-m-d') : ''; 358 342 if ($changed_after) { … … 360 344 } 361 345 346 // No overfetch, no PHP filtering, no array_slice needed 362 347 $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(); 380 350 } 381 351 -
adscale-ai/trunk/src/PluginApi/Products.php
r3429458 r3457682 277 277 278 278 $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 280 297 $variations_data = []; 281 298 282 299 $variations_objs = wc_get_products( 283 300 [ … … 292 309 ] 293 310 ); 294 311 295 312 $variations_objs_filtered = []; 296 313 if ( $variations_objs && is_array( $variations_objs ) ) { 297 314 foreach ( $variations_objs as $variation ) { 298 315 299 316 // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. 300 317 if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { … … 309 326 continue; 310 327 } 311 328 312 329 $variations_objs_filtered[] = $variation; 313 330 } 314 331 } 315 316 332 333 317 334 $variants_count_real = count( $variations_objs_filtered ); 318 335 $variants_count_real = $variants_count_real ? $variants_count_real : 1; 319 336 320 337 if ( $variants_count_real > self::$requestParams['variants_limit'] ) { 321 338 $max_variants_num = self::$requestParams['variants_limit']; 322 339 $cheapest_num = (int) floor( $max_variants_num / 2 ); 323 340 $expensive_num = $max_variants_num - $cheapest_num; 324 341 325 342 $variations_objs_ordered = wc_products_array_orderby( $variations_objs_filtered, 'price', 'ASC' ); 326 343 327 344 $variations_objs_cheapest = array_slice( $variations_objs_ordered, 0, $cheapest_num ); 328 345 $variations_objs_expensive = array_slice( $variations_objs_ordered, - $expensive_num ); 329 346 $variations_objs_resolved = array_merge( $variations_objs_cheapest, $variations_objs_expensive ); 330 347 331 348 } else { 332 349 $variations_objs_resolved = $variations_objs_filtered; 333 350 } 334 335 351 352 336 353 if ( $variations_objs_resolved ) { 337 354 foreach ( $variations_objs_resolved as $variation ) { … … 351 368 'keywords' => null, 352 369 'categories' => array_values( (array) $product->get_category_ids() ), 370 'tags' => array_values( (array) $tag_names ), 371 'brands' => $brand_names, 353 372 'attributes' => self::getAttr($product), 354 373 'variants_count_real' => $variants_count_real,
Note: See TracChangeset
for help on using the changeset viewer.