| @@ -137,9 +137,8 @@ | ||
| 137 | 137 | // Pagination. |
| 138 | 138 | 'pagination' => ! empty( $meta['show_pagination'] ), |
| 139 | 139 | 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'pagination' ), |
| 140 | 140 | 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ) ?: ( ! empty( $meta['posts_per_page'] ) ? absint( $meta['posts_per_page'] ) : '' ), |
| 141 | - 'visible_range' => max( 1, absint( self::get_data( $meta, 'pagination_visible_range', 5 ) ) ), | |
| 142 | 141 | |
| 143 | 142 | // Image. |
| 144 | 143 | 'f_img' => ! empty( $meta['show_featured_image'] ), |
| 145 | 144 | 'gallery_img' => ! empty( $meta['show_product_gallery'] ), |
| @@ -281,13 +280,9 @@ | ||
| 281 | 280 | } |
| 282 | 281 | } |
| 283 | 282 | |
| 284 | 283 | if ( is_search() ) { |
| 285 | - // Ask for the unescaped term: this dataset is data, not output. | |
| 286 | - // get_search_query() defaults to esc_attr(), and pre-escaping here | |
| 287 | - // would be encoded a second time at the output boundary and would | |
| 288 | - // corrupt the JSON payload this array is encoded into. | |
| 289 | - $search_query = get_search_query( false ); | |
| 284 | + $search_query = get_search_query(); | |
| 290 | 285 | |
| 291 | 286 | if ( $search_query ) { |
| 292 | 287 | $data['search_query'] = sanitize_text_field( $search_query ); |
| 293 | 288 | } |
| @@ -354,13 +349,9 @@ | ||
| 354 | 349 | } |
| 355 | 350 | } |
| 356 | 351 | |
| 357 | 352 | if ( is_search() ) { |
| 358 | - // Ask for the unescaped term: this dataset is data, not output. | |
| 359 | - // get_search_query() defaults to esc_attr(), and pre-escaping here | |
| 360 | - // would be encoded a second time at the output boundary and would | |
| 361 | - // corrupt the JSON payload this array is encoded into. | |
| 362 | - $search_query = get_search_query( false ); | |
| 353 | + $search_query = get_search_query(); | |
| 363 | 354 | |
| 364 | 355 | if ( $search_query ) { |
| 365 | 356 | $data['search_query'] = sanitize_text_field( $search_query ); |
| 366 | 357 | } |
| @@ -543,18 +534,9 @@ | ||
| 543 | 534 | */ |
| 544 | 535 | public static function pagination_data( $metas, $template ) { |
| 545 | 536 | $el_data = self::meta_dataset( $metas, $template ); |
| 546 | 537 | |
| 547 | - // Returned as an attribute *value*; the caller escapes it for the | |
| 548 | - // attribute context (Elementor's Utils::render_html_attributes() applies | |
| 549 | - // esc_attr()). esc_js() here only double-encoded the payload and, via | |
| 550 | - // addslashes(), corrupted the JSON escapes for apostrophes and | |
| 551 | - // non-ASCII search terms. The HTML-safe flags keep the payload free of | |
| 552 | - // characters esc_attr() would have to encode - see self::archive_data(). | |
| 553 | - return wp_json_encode( | |
| 554 | - $el_data, | |
| 555 | - JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | |
| 556 | - ); | |
| 538 | + return esc_js( wp_json_encode( $el_data, JSON_UNESCAPED_UNICODE ) ); | |
| 557 | 539 | } |
| 558 | 540 | |
| 559 | 541 | /** |
| 560 | 542 | * Container classes |
| @@ -608,12 +590,8 @@ | ||
| 608 | 590 | $action_btn_classes = [ |
| 609 | 591 | 'show_compare_text' => 'show-compare-text', |
| 610 | 592 | 'show_quick_view_text' => 'show-quick-view-text', |
| 611 | 593 | 'show_wishlist_text' => 'show-wishlist-text', |
| 612 | - // Quick checkout only had its icon entry here, so the "Show Button Text?" | |
| 613 | - // switch had nothing to emit and the label stayed hidden whatever it was | |
| 614 | - // set to. | |
| 615 | - 'show_quick_checkout_text' => 'show-quick-checkout-text', | |
| 616 | 594 | 'show_compare_icon' => 'no-compare-icon', |
| 617 | 595 | 'show_quick_view_icon' => 'no-quick-view-icon', |
| 618 | 596 | 'show_wishlist_icon' => 'no-wishlist-icon', |
| 619 | 597 | 'show_quick_checkout_icon' => 'no-quick_checkout-icon', |
| @@ -659,53 +637,15 @@ | ||
| 659 | 637 | if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts( $layout ) ), true ) ) { |
| 660 | 638 | $layout = self::set_default_layout( $layout ); |
| 661 | 639 | } |
| 662 | 640 | |
| 663 | - return self::resolve_existing_layout( $layout, $template ); | |
| 641 | + return $layout; | |
| 664 | 642 | } else { |
| 665 | - return ! empty( $template ) ? self::resolve_existing_layout( self::set_default_layout( $layout ), $template ) : $default; | |
| 643 | + return ! empty( $template ) ? self::set_default_layout( $layout ) : $default; | |
| 666 | 644 | } |
| 667 | 645 | } |
| 668 | 646 | |
| 669 | 647 | /** |
| 670 | - * Falls back to the default layout when the resolved template file cannot | |
| 671 | - * be found. | |
| 672 | - * | |
| 673 | - * Layouts contributed by a theme or an addon (rtsb-*, toyup-*, zilly-*) | |
| 674 | - * stay saved in the Elementor document after that theme or addon is gone. | |
| 675 | - * Without this check the loop asks for a template that no longer exists, | |
| 676 | - * so nothing renders and load_template() raises a "does not exist" notice | |
| 677 | - * for every product in the loop. | |
| 678 | - * | |
| 679 | - * @param string $layout Resolved layout name. | |
| 680 | - * @param string $template Template directory, e.g. "elementor/general/grid/". | |
| 681 | - * Empty when the layout is only used for CSS classes. | |
| 682 | - * | |
| 683 | - * @return string | |
| 684 | - */ | |
| 685 | - private static function resolve_existing_layout( $layout, $template ) { | |
| 686 | - if ( empty( $template ) || empty( $layout ) ) { | |
| 687 | - return $layout; | |
| 688 | - } | |
| 689 | - | |
| 690 | - $cache_key = 'layout_exists_' . $template . $layout; | |
| 691 | - | |
| 692 | - if ( isset( self::$cache[ $cache_key ] ) ) { | |
| 693 | - return self::$cache[ $cache_key ]; | |
| 694 | - } | |
| 695 | - | |
| 696 | - $resolved = $layout; | |
| 697 | - | |
| 698 | - if ( ! Fns::locate_template( $template . $layout ) ) { | |
| 699 | - $resolved = self::set_default_layout( $layout ); | |
| 700 | - } | |
| 701 | - | |
| 702 | - self::$cache[ $cache_key ] = $resolved; | |
| 703 | - | |
| 704 | - return $resolved; | |
| 705 | - } | |
| 706 | - | |
| 707 | - /** | |
| 708 | 648 | * Row classes |
| 709 | 649 | * |
| 710 | 650 | * @param array $settings Settings array. |
| 711 | 651 | * |
| @@ -795,23 +735,9 @@ | ||
| 795 | 735 | */ |
| 796 | 736 | public static function archive_data( $metas, $template ) { |
| 797 | 737 | $el_data = self::archive_meta_dataset( $metas, $template ); |
| 798 | 738 | |
| 799 | - // The return value is an HTML attribute, so it must be escaped for the | |
| 800 | - // HTML attribute context. esc_js() is a JavaScript-string escaper: it | |
| 801 | - // turns ' back into a bare apostrophe and relies on a backslash | |
| 802 | - // that means nothing to an HTML parser, which let a search term close | |
| 803 | - // the attribute and inject its own. esc_attr() encodes both quote | |
| 804 | - // characters, so the value can never terminate the attribute. | |
| 805 | - // | |
| 806 | - // The JSON is additionally encoded with the HTML-safe flags so it holds | |
| 807 | - // no <, >, &, ' or " at all. That keeps a search term that itself looks | |
| 808 | - // like an entity (e.g. ") intact: esc_attr() does not double-encode | |
| 809 | - // an existing entity, so without this the browser would decode the | |
| 810 | - // term back into a raw quote and break JSON.parse(). | |
| 811 | - $json = wp_json_encode( $el_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); | |
| 812 | - | |
| 813 | - return sprintf( ' data-rtsb-ajax="%s"', esc_attr( $json ) ); | |
| 739 | + return ' data-rtsb-ajax=\'' . esc_js( wp_json_encode( $el_data ) ) . '\''; | |
| 814 | 740 | } |
| 815 | 741 | |
| 816 | 742 | /** |
| 817 | 743 | * Slider options. |
| @@ -1388,49 +1314,52 @@ | ||
| 1388 | 1314 | // Archive-scope only when the URL path itself is a term archive |
| 1389 | 1315 | // (e.g. /product-category/clothing/). On the shop page WordPress can |
| 1390 | 1316 | // resolve ?product_cat=X to a queried term, but in that case we want |
| 1391 | 1317 | // the full hierarchy to remain visible for additional filtering. |
| 1392 | - $archive_term = self::request_path_is_term_archive( $taxonomy ) ? self::resolve_archive_term_from_path( $taxonomy ) : null; | |
| 1318 | + $is_term_archive_url = self::request_path_is_term_archive( $taxonomy ); | |
| 1393 | 1319 | |
| 1394 | - // The cache key MUST include the archive term id. The scoped list is | |
| 1395 | - // different for every term archive, so a taxonomy-only key leaks the | |
| 1396 | - // first visited category's term list into every other category archive | |
| 1397 | - // when a persistent object cache is active - the leaked terms are then | |
| 1398 | - // counted as empty in the current archive and the whole filter renders | |
| 1399 | - // blank. | |
| 1400 | - $cache_key = $archive_term instanceof WP_Term | |
| 1401 | - ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy . '_' . $archive_term->term_id | |
| 1320 | + $cache_key = $is_term_archive_url | |
| 1321 | + ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy | |
| 1402 | 1322 | : 'rtsb_get_terms_filter_products_full_' . $taxonomy; |
| 1323 | + $taxonomy_terms = wp_cache_get( $cache_key, 'shopbuilder' ); | |
| 1403 | 1324 | |
| 1404 | - $taxonomy_terms = wp_cache_get( $cache_key, Cache::group() ); | |
| 1325 | + if ( false === $taxonomy_terms ) { | |
| 1326 | + $archive_term = $is_term_archive_url ? self::resolve_archive_term_from_path( $taxonomy ) : null; | |
| 1405 | 1327 | |
| 1406 | - if ( false === $taxonomy_terms ) { | |
| 1407 | 1328 | if ( $archive_term instanceof WP_Term ) { |
| 1408 | - // Treat the URL-derived term as the root of the visible | |
| 1409 | - // hierarchy so the recursive renderer in | |
| 1410 | - // get_product_default_filter_list_html() (which starts | |
| 1411 | - // with $parent = 0) picks it up on child-category | |
| 1412 | - // archives. Clone first to avoid mutating WP's term | |
| 1413 | - // object. | |
| 1414 | - $current_term = clone $archive_term; | |
| 1415 | - $current_term->parent = 0; | |
| 1416 | - $taxonomy_terms = [ $current_term ]; | |
| 1417 | - $child_args = [ | |
| 1418 | - 'taxonomy' => $taxonomy, | |
| 1419 | - 'child_of' => $archive_term->term_id, | |
| 1420 | - 'hide_empty' => false, | |
| 1421 | - ]; | |
| 1329 | + $term_id = $archive_term->term_id; | |
| 1330 | + $archive_cache_key = 'rtsb_get_terms_filter_products_' . $taxonomy . $term_id; | |
| 1331 | + $taxonomy_terms = wp_cache_get( $archive_cache_key, 'shopbuilder' ); | |
| 1332 | + if ( false === $taxonomy_terms ) { | |
| 1333 | + // Treat the URL-derived term as the root of the visible | |
| 1334 | + // hierarchy so the recursive renderer in | |
| 1335 | + // get_product_default_filter_list_html() (which starts | |
| 1336 | + // with $parent = 0) picks it up on child-category | |
| 1337 | + // archives. Clone first to avoid mutating WP's term | |
| 1338 | + // object. | |
| 1339 | + $current_term = clone $archive_term; | |
| 1340 | + $current_term->parent = 0; | |
| 1341 | + $taxonomy_terms = [ $current_term ]; | |
| 1342 | + $child_args = [ | |
| 1343 | + 'taxonomy' => $taxonomy, | |
| 1344 | + 'child_of' => $term_id, | |
| 1345 | + 'hide_empty' => false, | |
| 1346 | + ]; | |
| 1422 | 1347 | |
| 1423 | - $child_terms = get_terms( $child_args ); | |
| 1348 | + $child_terms = get_terms( $child_args ); | |
| 1424 | 1349 | |
| 1425 | - if ( ! empty( $child_terms ) && ! is_wp_error( $child_terms ) ) { | |
| 1426 | - $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms ); | |
| 1350 | + if ( ! empty( $child_terms ) ) { | |
| 1351 | + $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms ); | |
| 1352 | + } | |
| 1353 | + | |
| 1354 | + wp_cache_set( $archive_cache_key, $taxonomy_terms, 'shopbuilder' ); | |
| 1355 | + Cache::set_data_cache_key( $archive_cache_key ); | |
| 1427 | 1356 | } |
| 1428 | 1357 | } else { |
| 1429 | 1358 | $taxonomy_terms = get_terms( $args ); |
| 1430 | 1359 | } |
| 1431 | 1360 | |
| 1432 | - wp_cache_set( $cache_key, $taxonomy_terms, Cache::group(), 12 * HOUR_IN_SECONDS ); | |
| 1361 | + wp_cache_set( $cache_key, $taxonomy_terms, 'shopbuilder' ); | |
| 1433 | 1362 | Cache::set_data_cache_key( $cache_key ); |
| 1434 | 1363 | } |
| 1435 | 1364 | |
| 1436 | 1365 | return $taxonomy_terms; |
| @@ -1772,20 +1701,8 @@ | ||
| 1772 | 1701 | $taxonomy = $tax->taxonomy; |
| 1773 | 1702 | $term_id = $tax->term_id; |
| 1774 | 1703 | } |
| 1775 | 1704 | |
| 1776 | - // A taxonomy selected through the query string is a removable choice, | |
| 1777 | - // not a fixed archive context, and the filter ORs terms within one | |
| 1778 | - // taxonomy. Scoping a term's count to that selection therefore | |
| 1779 | - // advertises a result the filter never produces - on | |
| 1780 | - // /shop/?product_cat=aliens the Movies count read 32 while choosing | |
| 1781 | - // Movies returns 279 - and it hides every sibling term whose overlap | |
| 1782 | - // with the selection is zero. Only a real term-archive URL | |
| 1783 | - // (/product-category/aliens/) is a fixed context worth scoping to. | |
| 1784 | - if ( $taxonomy === $page->taxonomy && ! self::request_path_is_term_archive( $page->taxonomy ) ) { | |
| 1785 | - return $count; | |
| 1786 | - } | |
| 1787 | - | |
| 1788 | 1705 | if ( strpos( $page->taxonomy, 'pa_' ) !== false ) { |
| 1789 | 1706 | $args['status'] = 'publish'; |
| 1790 | 1707 | $args['limit'] = -1; |
| 1791 | 1708 | $args['return'] = 'ids'; |
| @@ -1810,9 +1727,12 @@ | ||
| 1810 | 1727 | 'operator' => 'NOT IN', |
| 1811 | 1728 | ], |
| 1812 | 1729 | ]; |
| 1813 | 1730 | |
| 1814 | - return Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1731 | + $query = new WC_Product_Query( $args ); | |
| 1732 | + $products = $query->get_products(); | |
| 1733 | + | |
| 1734 | + return count( $products ); | |
| 1815 | 1735 | } |
| 1816 | 1736 | |
| 1817 | 1737 | // Scope the count to the current archive term (category, brand, tag, etc.) |
| 1818 | 1738 | // so it reflects only products that are in this archive AND also have the |
| @@ -1843,9 +1763,12 @@ | ||
| 1843 | 1763 | ], |
| 1844 | 1764 | ], |
| 1845 | 1765 | ]; |
| 1846 | 1766 | |
| 1847 | - return Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1767 | + $query = new WC_Product_Query( $args ); | |
| 1768 | + $products = $query->get_products(); | |
| 1769 | + | |
| 1770 | + return count( $products ); | |
| 1848 | 1771 | } |
| 1849 | 1772 | |
| 1850 | 1773 | /** |
| 1851 | 1774 | * Get the count of visible (non-hidden) products for a given taxonomy term. |
| @@ -1886,9 +1809,10 @@ | ||
| 1886 | 1809 | ], |
| 1887 | 1810 | ], |
| 1888 | 1811 | ]; |
| 1889 | 1812 | |
| 1890 | - $count = Fns::count_products_by_tax_query( $args['tax_query'] ); | |
| 1813 | + $query = new WC_Product_Query( $args ); | |
| 1814 | + $count = count( $query->get_products() ); | |
| 1891 | 1815 | |
| 1892 | 1816 | $cache[ $cache_key ] = $count; |
| 1893 | 1817 | |
| 1894 | 1818 | return $count; |