Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Results_Sort
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
7 / 7
17
100.00% covered (success)
100.00%
1 / 1
 get_all_option_keys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_option_labels
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 normalize_default_sort
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 resolve_available_options
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 normalize_display_as
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 resolve_label
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parse_url_sort
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Results-sort block helpers.
4 *
5 * @package automattic/jetpack-search
6 */
7
8namespace Automattic\Jetpack\Search;
9
10/**
11 * Helpers for `jetpack-search/results-sort` — sort keys, translated labels,
12 * and attribute normalization shared between render.php and tests. Product
13 * keys (`rating_desc`, `price_asc`, `price_desc`) are gated on WooCommerce
14 * so non-Woo deep links can't reach the rendered control.
15 */
16class Results_Sort {
17
18    const BASE_SORT_KEYS = array( 'relevance', 'newest', 'oldest' );
19
20    /**
21     * Product keys, gated on Woo. Order is meaningful — `rating` leads
22     * because it's the most common default for product pages.
23     */
24    const PRODUCT_SORT_KEYS = array( 'rating_desc', 'price_asc', 'price_desc' );
25
26    /**
27     * All keys the block may render. Order is meaningful (option/radio sequence).
28     *
29     * @return string[]
30     */
31    public static function get_all_option_keys(): array {
32        if ( Search_Blocks::woocommerce_blocks_enabled() ) {
33            return array_merge( self::BASE_SORT_KEYS, self::PRODUCT_SORT_KEYS );
34        }
35        return self::BASE_SORT_KEYS;
36    }
37
38    /**
39     * Translated label per sort key. Accessor (not a constant) so strings go
40     * through `__()` at call time.
41     *
42     * @return array<string, string>
43     */
44    public static function get_option_labels(): array {
45        return array(
46            'relevance'   => __( 'Relevance', 'jetpack-search-pkg' ),
47            'newest'      => __( 'Newest', 'jetpack-search-pkg' ),
48            'oldest'      => __( 'Oldest', 'jetpack-search-pkg' ),
49            'rating_desc' => __( 'Rating', 'jetpack-search-pkg' ),
50            'price_asc'   => __( 'Price: low to high', 'jetpack-search-pkg' ),
51            'price_desc'  => __( 'Price: high to low', 'jetpack-search-pkg' ),
52        );
53    }
54
55    /**
56     * Normalize `defaultSort`. Unknown values collapse to `relevance` so the
57     * fallback matches `parse_url_sort()` and `DEFAULT_SORT_ORDER` in JS.
58     *
59     * @param array $attributes Block attributes.
60     * @return string
61     */
62    public static function normalize_default_sort( array $attributes ): string {
63        $candidate = (string) ( $attributes['defaultSort'] ?? 'relevance' );
64        return in_array( $candidate, self::get_all_option_keys(), true ) ? $candidate : 'relevance';
65    }
66
67    /**
68     * Resolve the ordered sort keys to render. Canonical order from
69     * `get_all_option_keys()` wins (stable UI across saves). Empty / all-unknown
70     * falls back to the full set so a misconfigured block never renders zero options.
71     *
72     * @param array $attributes Block attributes.
73     * @return string[]
74     */
75    public static function resolve_available_options( array $attributes ): array {
76        $all      = self::get_all_option_keys();
77        $provided = $attributes['availableSortOptions'] ?? null;
78        if ( ! is_array( $provided ) ) {
79            return $all;
80        }
81        $allowed = array_values(
82            array_filter(
83                $all,
84                static function ( $key ) use ( $provided ) {
85                    return in_array( $key, $provided, true );
86                }
87            )
88        );
89        if ( empty( $allowed ) ) {
90            return $all;
91        }
92        return $allowed;
93    }
94
95    /**
96     * Normalize `displayAs`. Unknown collapses to `select`.
97     *
98     * @param array $attributes Block attributes.
99     * @return string 'select', 'radio', or 'popover'.
100     */
101    public static function normalize_display_as( array $attributes ): string {
102        $candidate = (string) ( $attributes['displayAs'] ?? 'select' );
103        if ( in_array( $candidate, array( 'radio', 'popover' ), true ) ) {
104            return $candidate;
105        }
106        $legacy_candidate = (string) ( $attributes['display'] ?? 'select' );
107        return 'popover' === $legacy_candidate ? 'popover' : 'select';
108    }
109
110    /**
111     * User-visible label, defaulting to "Sort by" (pre-SEARCH-138 fallback for
112     * posts saved before the attribute existed).
113     *
114     * @param array $attributes Block attributes.
115     * @return string
116     */
117    public static function resolve_label( array $attributes ): string {
118        $label = trim( (string) ( $attributes['label'] ?? '' ) );
119        if ( '' === $label ) {
120            return __( 'Sort by', 'jetpack-search-pkg' );
121        }
122        return $label;
123    }
124
125    /**
126     * Read `?orderby=…` off the request. Extends `Search_Blocks::parse_url_sort()`
127     * by accepting every option this block may render — a radio UI can expose
128     * a product-format key the state-seeder doesn't recognize, and a deep link
129     * to that key should still select it.
130     *
131     * @param string[]|null $allowed_keys Restrict to this list (defaults to all).
132     * @return string|null Sort key, or null when no URL sort is present/recognized.
133     */
134    public static function parse_url_sort( ?array $allowed_keys = null ): ?string {
135        // `?orderby[]=x` arrives as an array; sanitize_key() warns on that. Bail.
136        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only URL state.
137        if ( ! isset( $_GET['orderby'] ) || ! is_scalar( $_GET['orderby'] ) ) {
138            return null;
139        }
140        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only URL state.
141        $raw  = sanitize_key( wp_unslash( $_GET['orderby'] ) );
142        $pool = $allowed_keys ?? self::get_all_option_keys();
143        return in_array( $raw, $pool, true ) ? $raw : null;
144    }
145}