Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.12% covered (danger)
3.12%
18 / 576
0.00% covered (danger)
0.00%
0 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Search_Widget
2.96% covered (danger)
2.96%
17 / 574
0.00% covered (danger)
0.00%
0 / 24
17036.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
6.12
 is_search_active
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 activate_search
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 widget_admin_setup
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_frontend_scripts
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 get_sort_types
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 is_for_current_widget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 should_display_sitewide_filters
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 jetpack_search_populate_defaults
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 populate_defaults_for_instant_search
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 widget
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 widget_non_instant
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
306
 widget_instant
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 widget_empty_instant
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 maybe_render_sort_javascript
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 sorting_to_wp_query_param
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 update
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
506
 maybe_reformat_widget
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
182
 form
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 1
110
 form_for_instant_search
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 render_widget_attr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 render_widget_option_selected
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 render_widget_edit_filter
0.00% covered (danger)
0.00%
0 / 134
0.00% covered (danger)
0.00%
0 / 1
182
 get_allowed_taxonomies_for_widget_filters
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jetpack Search widget.
4 *
5 * @package automattic/jetpack-search
6 */
7
8namespace Automattic\Jetpack\Search;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Automattic\Jetpack\Redirect;
13use Automattic\Jetpack\Status;
14use Automattic\Jetpack\Sync\Modules\Search as Search_Sync_Module;
15use Automattic\Jetpack\Tracking;
16
17if ( ! defined( 'ABSPATH' ) ) {
18    exit( 0 );
19}
20
21/**
22 * Provides a widget to show available/selected filters on searches.
23 *
24 * @since 5.0.0
25 *
26 * @see   WP_Widget
27 */
28class Search_Widget extends \WP_Widget {
29
30    /**
31     * Number of aggregations (filters) to show by default.
32     *
33     * @since 5.8.0
34     * @var int
35     */
36    const DEFAULT_FILTER_COUNT = 5;
37    /**
38     * Default sort order for search results.
39     *
40     * @since 5.8.0
41     * @var string
42     */
43    const DEFAULT_SORT = 'relevance_desc';
44    /**
45     * Never used.
46     *
47     * @since 5.7.0
48     * @deprecated 0.44.5
49     * @var null
50     */
51    protected $jetpack_search;
52    /**
53     * Module_Control instance
54     *
55     * @var Module_Control
56     */
57    protected $module_control;
58
59    /**
60     * Search_Widget constructor.
61     *
62     * @param string $name Widget name.
63     * @since 5.0.0
64     */
65    public function __construct( $name = null ) {
66        if ( empty( $name ) ) {
67            $name = esc_html__( 'Search (Jetpack)', 'jetpack-search-pkg' );
68        }
69        $this->module_control = new Module_Control();
70        parent::__construct(
71            Helper::FILTER_WIDGET_BASE,
72            $name,
73            array(
74                'classname'   => 'jetpack-filters widget_search',
75                'description' => __( 'Instant search and filtering to help visitors quickly find relevant answers and explore your site.', 'jetpack-search-pkg' ),
76            )
77        );
78
79        if (
80            Helper::is_active_widget( $this->id ) &&
81            ! $this->is_search_active()
82        ) {
83            $this->activate_search();
84        }
85
86        if ( is_admin() ) {
87            add_action( 'sidebar_admin_setup', array( $this, 'widget_admin_setup' ) );
88        }
89
90        add_action( 'jetpack_search_render_filters_widget_title', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_widget_title' ), 10, 3 );
91        if ( Options::is_instant_enabled() ) {
92            add_action( 'jetpack_search_render_filters', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_instant_filters' ), 10, 2 );
93        } else {
94            add_action( 'jetpack_search_render_filters', array( 'Automattic\Jetpack\Search\Template_Tags', 'render_available_filters' ), 10, 2 );
95        }
96    }
97
98    /**
99     * Check whether search is currently active
100     *
101     * @since 6.3
102     */
103    public function is_search_active() {
104        return $this->module_control->is_active();
105    }
106
107    /**
108     * Activate search
109     *
110     * @since 6.3
111     */
112    public function activate_search() {
113        return $this->module_control->activate();
114    }
115
116    /**
117     * Enqueues the scripts and styles needed for the customizer.
118     *
119     * @since 5.7.0
120     */
121    public function widget_admin_setup() {
122        // Register jp-tracks and jp-tracks-functions.
123        Tracking::register_tracks_functions_scripts();
124
125        Assets::register_script(
126            'jetpack-search-widget-admin',
127            'js/search-widget-admin.js',
128            __FILE__,
129            array(
130                'in_footer'    => true,
131                'textdomain'   => 'jetpack-search-pkg',
132                'css_path'     => 'css/search-widget-admin-ui.css',
133                'dependencies' => array( 'jquery', 'jquery-ui-sortable', 'jp-tracks-functions' ),
134            )
135        );
136
137        $dotcom_data = ( new Connection_Manager( Package::SLUG ) )->get_connected_user_data();
138
139        wp_localize_script(
140            'jetpack-search-widget-admin',
141            'jetpack_search_filter_admin',
142            array(
143                'defaultFilterCount' => self::DEFAULT_FILTER_COUNT,
144                'tracksUserData'     => ! empty( $dotcom_data ) ? array(
145                    'userid'   => $dotcom_data['ID'],
146                    'username' => $dotcom_data['login'],
147                ) : false,
148                'tracksEventData'    => array(
149                    'is_customizer' => (int) is_customize_preview(),
150                ),
151                'i18n'               => array(
152                    'month'        => Helper::get_date_filter_type_name( 'month', false ),
153                    'year'         => Helper::get_date_filter_type_name( 'year', false ),
154                    'monthUpdated' => Helper::get_date_filter_type_name( 'month', true ),
155                    'yearUpdated'  => Helper::get_date_filter_type_name( 'year', true ),
156                ),
157            )
158        );
159
160        Assets::enqueue_script( 'jetpack-search-widget-admin' );
161    }
162
163    /**
164     * Enqueue scripts and styles for the frontend.
165     *
166     * @since 5.8.0
167     */
168    public function enqueue_frontend_scripts() {
169        if ( Options::is_instant_enabled() ) {
170            return;
171        }
172        Assets::register_script(
173            'jetpack-search-widget',
174            'js/search-widget.js',
175            __FILE__,
176            array(
177                'in_footer'  => true,
178                'textdomain' => 'jetpack-search-pkg',
179                // Jetpack the plugin would concatenated the style with other styles and minimize. And the style would be dequeued from WP.
180                // @see https://github.com/Automattic/jetpack/blob/b3de78dce3d88b0d9b283282a5b04515245c8057/projects/plugins/jetpack/tools/builder/frontend-css.js#L52.
181                // @see https://github.com/Automattic/jetpack/blob/bb1b6a9a9cfa98600441f8fa31c9f9c4ef9a04a5/projects/plugins/jetpack/class.jetpack.php#L106.
182                'css_path'   => 'css/search-widget-frontend.css',
183                'enqueue'    => true,
184            )
185        );
186    }
187
188    /**
189     * Get the list of valid sort types/orders.
190     *
191     * @return array The sort orders.
192     * @since 5.8.0
193     */
194    private function get_sort_types() {
195        return array(
196            'relevance|DESC' => is_admin() ? esc_html__( 'Relevance (recommended)', 'jetpack-search-pkg' ) : esc_html__( 'Relevance', 'jetpack-search-pkg' ),
197            'date|DESC'      => esc_html__( 'Newest first', 'jetpack-search-pkg' ),
198            'date|ASC'       => esc_html__( 'Oldest first', 'jetpack-search-pkg' ),
199        );
200    }
201
202    /**
203     * Callback for an array_filter() call in order to only get filters for the current widget.
204     *
205     * @param array $item Filter item.
206     *
207     * @return bool Whether the current filter item is for the current widget.
208     * @see   Search_Widget::widget()
209     *
210     * @since 5.7.0
211     */
212    public function is_for_current_widget( $item ) {
213        return isset( $item['widget_id'] ) && $this->id == $item['widget_id']; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
214    }
215
216    /**
217     * This method returns a boolean for whether the widget should show site-wide filters for the site.
218     *
219     * This is meant to provide backwards-compatibility for VIP, and other professional plan users, that manually
220     * configured filters via `Automattic\Jetpack\Search\Classic_Search::set_filters()`.
221     *
222     * @return bool Whether the widget should display site-wide filters or not.
223     * @since 5.7.0
224     */
225    public function should_display_sitewide_filters() {
226        $filter_widgets = get_option( 'widget_jetpack-search-filters' );
227
228        // This shouldn't be empty, but just for sanity.
229        if ( empty( $filter_widgets ) ) {
230            return false;
231        }
232
233        // If any widget has any filters, return false.
234        foreach ( $filter_widgets as $number => $widget ) {
235            $widget_id = sprintf( '%s-%d', $this->id_base, $number );
236            if ( ! empty( $widget['filters'] ) && is_active_widget( false, $widget_id, $this->id_base ) ) {
237                return false;
238            }
239        }
240
241        return true;
242    }
243
244    /**
245     * Widget defaults.
246     *
247     *  @param array $instance Previously saved values from database.
248     */
249    public function jetpack_search_populate_defaults( $instance ) {
250        $instance = wp_parse_args(
251            (array) $instance,
252            array(
253                'title'              => '',
254                'search_box_enabled' => true,
255                'user_sort_enabled'  => true,
256                'sort'               => self::DEFAULT_SORT,
257                'filters'            => array( array() ),
258                'post_types'         => array(),
259            )
260        );
261
262        return $instance;
263    }
264
265    /**
266     * Populates the instance array with appropriate default values.
267     *
268     * @param array $instance Previously saved values from database.
269     * @return array Instance array with default values approprate for instant search
270     * @since 8.6.0
271     */
272    public function populate_defaults_for_instant_search( $instance ) {
273        return wp_parse_args(
274            (array) $instance,
275            array(
276                'title'   => '',
277                'filters' => array(),
278            )
279        );
280    }
281
282    /**
283     * Responsible for rendering the widget on the frontend.
284     *
285     * @param array $args     Widgets args supplied by the theme.
286     * @param array $instance The current widget instance.
287     * @since 5.0.0
288     */
289    public function widget( $args, $instance ) {
290        $instance = $this->jetpack_search_populate_defaults( $instance );
291
292        if ( ( new Status() )->is_offline_mode() ) {
293            echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
294            ?><div id="<?php echo esc_attr( $this->id ); ?>-wrapper">
295                <div class="jetpack-search-sort-wrapper">
296                    <label>
297                        <?php esc_html_e( 'Jetpack Search not supported in Offline Mode', 'jetpack-search-pkg' ); ?>
298                    </label>
299                </div>
300            </div>
301            <?php
302            echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
303            return;
304        }
305
306        // Enqueue front end assets.
307        $this->enqueue_frontend_scripts();
308
309        if ( Options::is_instant_enabled() ) {
310            if ( array_key_exists( 'id', $args ) && Instant_Search::INSTANT_SEARCH_SIDEBAR === $args['id'] ) {
311                $this->widget_empty_instant( $args, $instance );
312            } else {
313                $this->widget_instant( $args, $instance );
314            }
315        } else {
316            $this->widget_non_instant( $args, $instance );
317        }
318    }
319
320    /**
321     * Render the non-instant frontend widget.
322     *
323     * @param array $args     Widgets args supplied by the theme.
324     * @param array $instance The current widget instance.
325     * @since 8.3.0
326     */
327    public function widget_non_instant( $args, $instance ) {
328        $filters = array();
329
330        // Search instance must have been initialized before widget render.
331        if ( is_search() ) {
332            $search_instance = Inline_Search::get_instance_maybe_fallback_to_classic();
333            if ( Helper::should_rerun_search_in_customizer_preview() ) {
334                $search_instance->update_search_results_aggregations();
335            }
336
337            $filters = $search_instance->get_filters();
338
339            if ( ! Helper::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) {
340                $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) );
341            }
342        }
343
344        if ( ! $filters && empty( $instance['search_box_enabled'] ) && empty( $instance['user_sort_enabled'] ) ) {
345            return;
346        }
347
348        $title = ! empty( $instance['title'] ) ? $instance['title'] : '';
349
350        /** This filter is documented in core/src/wp-includes/default-widgets.php */
351        $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
352
353        echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
354        ?>
355            <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" >
356        <?php
357
358        if ( ! empty( $title ) ) {
359            /**
360             * Responsible for displaying the title of the Jetpack Search filters widget.
361             *
362             * @module search
363             *
364             * @param string $title                The widget's title
365             * @param string $args['before_title'] The HTML tag to display before the title
366             * @param string $args['after_title']  The HTML tag to display after the title
367             *@since  5.7.0
368             */
369            do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
370        }
371
372        $default_sort            = isset( $instance['sort'] ) ? $instance['sort'] : self::DEFAULT_SORT;
373        list( $orderby, $order ) = $this->sorting_to_wp_query_param( $default_sort );
374        $current_sort            = "{$orderby}|{$order}";
375
376        // we need to dynamically inject the sort field into the search box when the search box is enabled, and display
377        // it separately when it's not.
378        if ( ! empty( $instance['search_box_enabled'] ) ) {
379            Template_Tags::render_widget_search_form( $instance['post_types'], $orderby, $order );
380        }
381
382        if ( ! empty( $instance['search_box_enabled'] ) && ! empty( $instance['user_sort_enabled'] ) ) :
383            ?>
384                    <div class="jetpack-search-sort-wrapper">
385                <label>
386                    <?php esc_html_e( 'Sort by', 'jetpack-search-pkg' ); ?>
387                    <select class="jetpack-search-sort">
388                        <?php foreach ( $this->get_sort_types() as $sort => $label ) { ?>
389                            <option value="<?php echo esc_attr( $sort ); ?><?php selected( $current_sort, $sort ); ?>>
390                                <?php echo esc_html( $label ); ?>
391                            </option>
392                        <?php } ?>
393                    </select>
394                </label>
395            </div>
396            <?php
397        endif;
398
399        if ( $filters ) {
400            /**
401             * Responsible for rendering filters to narrow down search results.
402             *
403             * @module search
404             *
405             * @param array $filters    The possible filters for the current query.
406             * @param array $post_types An array of post types to limit filtering to.
407             *@since  5.8.0
408             */
409            do_action(
410                'jetpack_search_render_filters',
411                $filters,
412                isset( $instance['post_types'] ) ? $instance['post_types'] : null
413            );
414        }
415
416        $this->maybe_render_sort_javascript( $instance, $order, $orderby );
417
418        echo '</div>';
419        echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
420    }
421
422    /**
423     * Render the instant frontend widget.
424     *
425     * @param array $args     Widgets args supplied by the theme.
426     * @param array $instance The current widget instance.
427     * @since 8.3.0
428     */
429    public function widget_instant( $args, $instance ) {
430        // Exit early if search instance has not been initialized.
431        if ( ! Instant_Search::instance() ) {
432            return false;
433        }
434
435        if ( Helper::should_rerun_search_in_customizer_preview() ) {
436            Instant_Search::instance()->update_search_results_aggregations();
437        }
438
439        $filters = Instant_Search::instance()->get_filters();
440        if ( ! Helper::are_filters_by_widget_disabled() && ! $this->should_display_sitewide_filters() ) {
441            $filters = array_filter( $filters, array( $this, 'is_for_current_widget' ) );
442        }
443
444        $title = ! empty( $instance['title'] ) ? $instance['title'] : '';
445
446        /** This filter is documented in core/src/wp-includes/default-widgets.php */
447        $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
448
449        echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
450        ?>
451            <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" class="jetpack-instant-search-wrapper">
452        <?php
453
454        if ( ! empty( $title ) ) {
455            /**
456             * Responsible for displaying the title of the Jetpack Search filters widget.
457             *
458             * @module search
459             *
460             * @param string $title                The widget's title
461             * @param string $args['before_title'] The HTML tag to display before the title
462             * @param string $args['after_title']  The HTML tag to display after the title
463             *@since  5.7.0
464             */
465            do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
466        }
467
468        Template_Tags::render_widget_search_form( array(), '', '' );
469
470        if ( $filters ) {
471            /**
472             * Responsible for rendering filters to narrow down search results.
473             *
474             * @module search
475             *
476             * @param array $filters    The possible filters for the current query.
477             * @param array $post_types An array of post types to limit filtering to.
478             *@since  5.8.0
479             */
480            do_action(
481                'jetpack_search_render_filters',
482                $filters,
483                null
484            );
485        }
486
487        echo '</div>';
488        echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
489    }
490
491    /**
492     * Render the instant widget for the overlay.
493     *
494     * @param array $args     Widgets args supplied by the theme.
495     * @param array $instance The current widget instance.
496     * @since 8.3.0
497     */
498    public function widget_empty_instant( $args, $instance ) {
499        $title = isset( $instance['title'] ) ? $instance['title'] : '';
500
501        if ( empty( $title ) ) {
502            $title = '';
503        }
504
505        /** This filter is documented in core/src/wp-includes/default-widgets.php */
506        $title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
507
508        echo $args['before_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
509        ?>
510            <div id="<?php echo esc_attr( $this->id ); ?>-wrapper" class="jetpack-instant-search-wrapper">
511        <?php
512
513        if ( ! empty( $title ) ) {
514            /**
515             * Responsible for displaying the title of the Jetpack Search filters widget.
516             *
517             * @module search
518             *
519             * @param string $title                The widget's title
520             * @param string $args['before_title'] The HTML tag to display before the title
521             * @param string $args['after_title']  The HTML tag to display after the title
522             *@since  5.7.0
523             */
524            do_action( 'jetpack_search_render_filters_widget_title', $title, $args['before_title'], $args['after_title'] );
525        }
526
527        echo '</div>';
528        echo $args['after_widget']; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
529    }
530
531    /**
532     * Renders JavaScript for the sorting controls on the frontend.
533     *
534     * This JS is a bit complicated, but here's what it's trying to do:
535     * - find the search form
536     * - find the orderby/order fields and set default values
537     * - detect changes to the sort field, if it exists, and use it to set the order field values
538     *
539     * @param array  $instance The current widget instance.
540     * @param string $order    The order to initialize the select with.
541     * @param string $orderby  The orderby to initialize the select with.
542     * @since 5.8.0
543     */
544    private function maybe_render_sort_javascript( $instance, $order, $orderby ) {
545        if ( Options::is_instant_enabled() ) {
546            return;
547        }
548
549        if ( ! empty( $instance['user_sort_enabled'] ) ) :
550            ?>
551        <script type="text/javascript">
552            var jetpackSearchModuleSorting = function() {
553                var orderByDefault = '<?php echo 'date' === $orderby ? 'date' : 'relevance'; ?>',
554                    orderDefault   = '<?php echo 'ASC' === $order ? 'ASC' : 'DESC'; ?>',
555                    widgetId       = decodeURIComponent( '<?php echo rawurlencode( $this->id ); ?>' ),
556                    searchQuery    = decodeURIComponent( '<?php echo rawurlencode( get_query_var( 's', '' ) ); ?>' ),
557                    isSearch       = <?php echo (int) is_search(); ?>;
558
559                var container = document.getElementById( widgetId + '-wrapper' ),
560                    form = container.querySelector( '.jetpack-search-form form' ),
561                    orderBy = form.querySelector( 'input[name=orderby]' ),
562                    order = form.querySelector( 'input[name=order]' ),
563                    searchInput = form.querySelector( 'input[name="s"]' ),
564                    sortSelectInput = container.querySelector( '.jetpack-search-sort' );
565
566                orderBy.value = orderByDefault;
567                order.value = orderDefault;
568
569                // Some themes don't set the search query, which results in the query being lost
570                // when doing a sort selection. So, if the query isn't set, let's set it now. This approach
571                // is chosen over running a regex over HTML for every search query performed.
572                if ( isSearch && ! searchInput.value ) {
573                    searchInput.value = searchQuery;
574                }
575
576                searchInput.classList.add( 'show-placeholder' );
577
578                sortSelectInput.addEventListener( 'change', function( event ) {
579                    var values  = event.target.value.split( '|' );
580                    orderBy.value = values[0];
581                    order.value = values[1];
582
583                    form.submit();
584                } );
585            }
586
587            if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
588                jetpackSearchModuleSorting();
589            } else {
590                document.addEventListener( 'DOMContentLoaded', jetpackSearchModuleSorting );
591            }
592            </script>
593            <?php
594        endif;
595    }
596
597    /**
598     * Convert a sort string into the separate order by and order parts.
599     *
600     * @param string $sort A sort string.
601     *
602     * @return array Order by and order.
603     * @since 5.8.0
604     */
605    private function sorting_to_wp_query_param( $sort ) {
606        // phpcs:disable WordPress.Security.NonceVerification.Recommended
607        $parts   = explode( '|', $sort );
608        $orderby = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] )
609            ? sanitize_sql_orderby( wp_unslash( $_GET['orderby'] ) )
610            : $parts[0];
611
612        $order = isset( $_GET['order'] ) && is_string( $_GET['order'] )
613            ? ( strtoupper( $_GET['order'] ) === 'ASC' ? 'ASC' : 'DESC' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is validating.
614            : ( ( isset( $parts[1] ) && 'ASC' === strtoupper( $parts[1] ) ) ? 'ASC' : 'DESC' );
615
616        // phpcs:enable WordPress.Security.NonceVerification.Recommended
617
618        return array( $orderby, $order );
619    }
620
621    /**
622     * Updates a particular instance of the widget. Validates and sanitizes the options.
623     *
624     * @param array $new_instance New settings for this instance as input by the user via Search_Widget::form().
625     * @param array $old_instance Old settings for this instance.
626     *
627     * @return array Settings to save.
628     * @since 5.0.0
629     */
630    public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
631        $new_instance = $this->maybe_reformat_widget( $new_instance );
632        $instance     = array();
633
634        $instance['title'] = sanitize_text_field( $new_instance['title'] );
635
636        // Keep `search_box_enabled` and `user_sort_enabled` settings when updating widget on Instant Search
637        // Set `search_box_enabled` and `user_sort_enabled` default to '1' when createing a NEW widget
638        if ( Options::is_instant_enabled() ) {
639            $instance['search_box_enabled'] = empty( $old_instance ) || empty( $old_instance['search_box_enabled'] ) ? '1' : $old_instance['search_box_enabled'];
640            $instance['user_sort_enabled']  = empty( $old_instance ) || empty( $old_instance['user_sort_enabled'] ) ? '1' : $old_instance['user_sort_enabled'];
641        } else {
642            $instance['search_box_enabled'] = empty( $new_instance['search_box_enabled'] ) ? '0' : '1';
643            $instance['user_sort_enabled']  = empty( $new_instance['user_sort_enabled'] ) ? '0' : '1';
644        }
645
646        $instance['sort']       = empty( $new_instance['sort'] ) ? self::DEFAULT_SORT : $new_instance['sort'];
647        $instance['post_types'] = empty( $new_instance['post_types'] ) || empty( $instance['search_box_enabled'] )
648            ? array()
649            : array_map( 'sanitize_key', $new_instance['post_types'] );
650
651        $filters = array();
652        if ( isset( $new_instance['filter_type'] ) ) {
653            foreach ( (array) $new_instance['filter_type'] as $index => $type ) {
654                $count = (int) ( $new_instance['num_filters'][ $index ] ?? 1 );
655                $count = min( 50, $count ); // Set max boundary at 50.
656                $count = max( 1, $count );  // Set min boundary at 1.
657
658                switch ( $type ) {
659                    case 'taxonomy':
660                        $filters[] = array(
661                            'name'     => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
662                            'type'     => 'taxonomy',
663                            'taxonomy' => sanitize_key( $new_instance['taxonomy_type'][ $index ] ?? '' ),
664                            'count'    => $count,
665                        );
666                        break;
667                    case 'post_type':
668                        $filters[] = array(
669                            'name'  => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
670                            'type'  => 'post_type',
671                            'count' => $count,
672                        );
673                        break;
674                    case 'author':
675                        $filters[] = array(
676                            'name'  => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
677                            'type'  => 'author',
678                            'count' => $count,
679                        );
680                        break;
681                    case 'blog_id':
682                        $filters[] = array(
683                            'name'  => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
684                            'type'  => 'blog_id',
685                            'count' => $count,
686                        );
687                        break;
688                    case 'date_histogram':
689                        $filters[] = array(
690                            'name'     => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
691                            'type'     => 'date_histogram',
692                            'count'    => $count,
693                            'field'    => sanitize_key( $new_instance['date_histogram_field'][ $index ] ?? '' ),
694                            'interval' => sanitize_key( $new_instance['date_histogram_interval'][ $index ] ?? '' ),
695                        );
696                        break;
697                    case 'product_attribute':
698                        $filter_data = array(
699                            'name'  => sanitize_text_field( $new_instance['filter_name'][ $index ] ?? '' ),
700                            'type'  => 'product_attribute',
701                            'count' => $count,
702                        );
703                        // Save included attributes if any are selected.
704                        if ( isset( $new_instance[ 'included_attributes_' . $index ] ) && is_array( $new_instance[ 'included_attributes_' . $index ] ) ) {
705                            $filter_data['included_attributes'] = array_map( 'sanitize_key', $new_instance[ 'included_attributes_' . $index ] );
706                        }
707                        $filters[] = $filter_data;
708                        break;
709                }
710            }
711        }
712
713        if ( ! empty( $filters ) ) {
714            $instance['filters'] = $filters;
715        }
716
717        return $instance;
718    }
719
720    /**
721     * Reformats the widget instance array to one that is recognized by the `update` function.
722     * This is only necessary when handling changes from the block-based widget editor.
723     *
724     * @param array $widget_instance - Jetpack Search widget instance.
725     *
726     * @return array - Potentially reformatted instance compatible with the save function.
727     */
728    protected function maybe_reformat_widget( $widget_instance ) {
729        if ( isset( $widget_instance['filter_type'] ) || ! isset( $widget_instance['filters'] ) || ! is_array( $widget_instance['filters'] ) ) {
730            return $widget_instance;
731        }
732
733        $instance = $widget_instance;
734        foreach ( $widget_instance['filters'] as $index => $filter ) {
735            $instance['filter_type'][]             = isset( $filter['type'] ) ? $filter['type'] : '';
736            $instance['taxonomy_type'][]           = isset( $filter['taxonomy'] ) ? $filter['taxonomy'] : '';
737            $instance['filter_name'][]             = isset( $filter['name'] ) ? $filter['name'] : '';
738            $instance['num_filters'][]             = isset( $filter['count'] ) ? $filter['count'] : 5;
739            $instance['date_histogram_field'][]    = isset( $filter['field'] ) ? $filter['field'] : '';
740            $instance['date_histogram_interval'][] = isset( $filter['interval'] ) ? $filter['interval'] : '';
741            // Handle included_attributes for product_attribute filters.
742            if ( isset( $filter['included_attributes'] ) && is_array( $filter['included_attributes'] ) ) {
743                $instance[ 'included_attributes_' . $index ] = $filter['included_attributes'];
744            }
745        }
746        unset( $instance['filters'] );
747        return $instance;
748    }
749
750    /**
751     * Outputs the settings update form.
752     *
753     * @param array $instance Previously saved values from database.
754     * @return string|void
755     * @since 5.0.0
756     */
757    public function form( $instance ) {
758        if ( Options::is_instant_enabled() ) {
759            return $this->form_for_instant_search( $instance );
760        }
761
762        $instance = $this->jetpack_search_populate_defaults( $instance );
763
764        $title = wp_strip_all_tags( $instance['title'] );
765
766        $hide_filters = Helper::are_filters_by_widget_disabled();
767
768        $classes = sprintf(
769            'jetpack-search-filters-widget %s %s %s',
770            $hide_filters ? 'hide-filters' : '',
771            $instance['search_box_enabled'] ? '' : 'hide-post-types',
772            $this->id
773        );
774        ?>
775        <div class="<?php echo esc_attr( $classes ); ?>">
776            <p>
777                <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
778                    <?php esc_html_e( 'Title (optional):', 'jetpack-search-pkg' ); ?>
779                </label>
780                <input
781                    class="widefat"
782                    id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
783                    name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
784                    type="text"
785                    value="<?php echo esc_attr( $title ); ?>"
786                />
787            </p>
788
789            <p>
790                <label>
791                    <input
792                        type="checkbox"
793                        class="jetpack-search-filters-widget__search-box-enabled"
794                        name="<?php echo esc_attr( $this->get_field_name( 'search_box_enabled' ) ); ?>"
795                        <?php checked( $instance['search_box_enabled'] ); ?>
796                    />
797                    <?php esc_html_e( 'Show search box', 'jetpack-search-pkg' ); ?>
798                </label>
799            </p>
800
801            <p>
802                <label>
803                    <input
804                        type="checkbox"
805                        class="jetpack-search-filters-widget__sort-controls-enabled"
806                        name="<?php echo esc_attr( $this->get_field_name( 'user_sort_enabled' ) ); ?>"
807                        <?php checked( $instance['user_sort_enabled'] ); ?>
808                        <?php disabled( ! $instance['search_box_enabled'] ); ?>
809                    />
810                    <?php esc_html_e( 'Show sort selection dropdown', 'jetpack-search-pkg' ); ?>
811                </label>
812            </p>
813
814            <p class="jetpack-search-filters-widget__post-types-select">
815                <label><?php esc_html_e( 'Post types to search (minimum of 1):', 'jetpack-search-pkg' ); ?></label>
816                <?php foreach ( get_post_types( array( 'exclude_from_search' => false ), 'objects' ) as $post_type ) : ?>
817                    <label>
818                        <input
819                            type="checkbox"
820                            value="<?php echo esc_attr( $post_type->name ); ?>"
821                            name="<?php echo esc_attr( $this->get_field_name( 'post_types' ) ); ?>[]"
822                            <?php checked( empty( $instance['post_types'] ) || in_array( $post_type->name, $instance['post_types'], true ) ); ?>
823                        />&nbsp;
824                        <?php echo esc_html( $post_type->label ); ?>
825                    </label>
826                <?php endforeach; ?>
827            </p>
828
829            <p>
830                <label>
831                    <?php esc_html_e( 'Default sort order:', 'jetpack-search-pkg' ); ?>
832                    <select
833                        name="<?php echo esc_attr( $this->get_field_name( 'sort' ) ); ?>"
834                        class="widefat jetpack-search-filters-widget__sort-order">
835                        <?php foreach ( $this->get_sort_types() as $sort_type => $label ) { ?>
836                            <option value="<?php echo esc_attr( $sort_type ); ?><?php selected( $instance['sort'], $sort_type ); ?>>
837                                <?php echo esc_html( $label ); ?>
838                            </option>
839                        <?php } ?>
840                    </select>
841                </label>
842            </p>
843
844            <?php if ( ! $hide_filters ) : ?>
845                <script class="jetpack-search-filters-widget__filter-template" type="text/template">
846                    <?php $this->render_widget_edit_filter( array(), true ); ?>
847                </script>
848                <div class="jetpack-search-filters-widget__filters">
849                    <?php
850                    $filter_index = 0;
851                    foreach ( (array) $instance['filters'] as $filter ) :
852                        $this->render_widget_edit_filter( $filter, false, false, $filter_index );
853                        ++$filter_index;
854                    endforeach;
855                    ?>
856                </div>
857                <p class="jetpack-search-filters-widget__add-filter-wrapper">
858                    <a class="button jetpack-search-filters-widget__add-filter" href="#">
859                        <?php esc_html_e( 'Add a filter', 'jetpack-search-pkg' ); ?>
860                    </a>
861                </p>
862                <noscript>
863                    <p class="jetpack-search-filters-help">
864                        <?php esc_html_e( 'Adding filters requires JavaScript!', 'jetpack-search-pkg' ); ?>
865                    </p>
866                </noscript>
867                <?php if ( is_customize_preview() ) : ?>
868                    <p class="jetpack-search-filters-help">
869                        <a href="<?php echo esc_url( Redirect::get_url( 'jetpack-support-search', array( 'anchor' => 'filters-not-showing-up' ) ) ); ?>" target="_blank">
870                            <?php esc_html_e( "Why aren't my filters appearing?", 'jetpack-search-pkg' ); ?>
871                        </a>
872                    </p>
873                <?php endif; ?>
874            <?php endif; ?>
875        </div>
876        <?php
877    }
878
879    /**
880     * Outputs the widget update form to be used in the Customizer for Instant Search.
881     *
882     * @param array $instance Previously saved values from database.
883     * @since 8.6.0
884     */
885    private function form_for_instant_search( $instance ) {
886        $instance = $this->populate_defaults_for_instant_search( $instance );
887        $classes  = sprintf( 'jetpack-search-filters-widget %s', $this->id );
888
889        ?>
890        <div class="<?php echo esc_attr( $classes ); ?>">
891            <!-- Title control -->
892            <p>
893                <label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
894                    <?php esc_html_e( 'Title (optional):', 'jetpack-search-pkg' ); ?>
895                </label>
896                <input
897                    class="widefat"
898                    id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
899                    name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
900                    type="text"
901                    value="<?php echo esc_attr( wp_strip_all_tags( $instance['title'] ) ); ?>"
902                />
903            </p>
904
905            <!-- Filters control -->
906            <?php if ( ! Helper::are_filters_by_widget_disabled() ) : ?>
907                <div class="jetpack-search-filters-widget__filters">
908                    <?php foreach ( (array) $instance['filters'] as $filter ) : ?>
909                        <?php $this->render_widget_edit_filter( $filter, false, true ); ?>
910                    <?php endforeach; ?>
911                </div>
912                <p class="jetpack-search-filters-widget__add-filter-wrapper">
913                    <a class="button jetpack-search-filters-widget__add-filter" href="#">
914                        <?php esc_html_e( 'Add a filter', 'jetpack-search-pkg' ); ?>
915                    </a>
916                </p>
917                <script class="jetpack-search-filters-widget__filter-template" type="text/template">
918                    <?php $this->render_widget_edit_filter( array(), true, true ); ?>
919                </script>
920                <noscript>
921                    <p class="jetpack-search-filters-help">
922                        <?php esc_html_e( 'Adding filters requires JavaScript!', 'jetpack-search-pkg' ); ?>
923                    </p>
924                </noscript>
925            <?php endif; ?>
926        </div>
927        <?php
928    }
929
930    /**
931     * We need to render HTML in two formats: an Underscore template (client-side)
932     * and native PHP (server-side). This helper function allows for easy rendering
933     * of attributes in both formats.
934     *
935     * @param string $name        Attribute name.
936     * @param string $value       Attribute value.
937     * @param bool   $is_template Whether this is for an Underscore template or not.
938     * @since 5.8.0
939     */
940    private function render_widget_attr( $name, $value, $is_template ) {
941        echo $is_template ? "<%= $name %>" : esc_attr( $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
942    }
943
944    /**
945     * We need to render HTML in two formats: an Underscore template (client-size)
946     * and native PHP (server-side). This helper function allows for easy rendering
947     * of the "selected" attribute in both formats.
948     *
949     * @param string $name        Attribute name.
950     * @param string $value       Attribute value.
951     * @param string $compare     Value to compare to the attribute value to decide if it should be selected.
952     * @param bool   $is_template Whether this is for an Underscore template or not.
953     * @since 5.8.0
954     */
955    private function render_widget_option_selected( $name, $value, $compare, $is_template ) {
956        $compare_js = rawurlencode( $compare );
957        echo $is_template ? "<%= decodeURIComponent( '$compare_js' ) === $name ? 'selected=\"selected\"' : '' %>" : selected( $value, $compare ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
958    }
959
960    /**
961     * Responsible for rendering a single filter in the customizer or the widget administration screen in wp-admin.
962     *
963     * We use this method for two purposes - rendering the fields server-side, and also rendering a script template for Underscore.
964     *
965     * @param array $filter      The filter to render.
966     * @param bool  $is_template Whether this is for an Underscore template or not.
967     * @param bool  $is_instant_search Whether this site enables Instant Search or not.
968     * @param int   $filter_index The index of this filter in the filters array.
969     * @since 5.7.0
970     */
971    public function render_widget_edit_filter( $filter, $is_template = false, $is_instant_search = false, $filter_index = 0 ) {
972        $args = wp_parse_args(
973            $filter,
974            array(
975                'name'      => '',
976                'type'      => 'taxonomy',
977                'taxonomy'  => '',
978                'post_type' => '',
979                'field'     => '',
980                'interval'  => '',
981                'count'     => self::DEFAULT_FILTER_COUNT,
982            )
983        );
984
985        $args['name_placeholder'] = Helper::generate_widget_filter_name( $args );
986
987        // Hide author & blog ID filters when Instant Search is turned off.
988        if ( ! $is_instant_search && in_array( $args['type'], array( 'author', 'blog_id' ), true ) ) :
989            return;
990        endif;
991        ?>
992        <div class="jetpack-search-filters-widget__filter is-<?php $this->render_widget_attr( 'type', $args['type'], $is_template ); ?>">
993            <p class="jetpack-search-filters-widget__type-select">
994                <label>
995                    <?php esc_html_e( 'Filter Type:', 'jetpack-search-pkg' ); ?>
996                    <select name="<?php echo esc_attr( $this->get_field_name( 'filter_type' ) ); ?>[]" class="widefat filter-select">
997                        <option value="taxonomy" <?php $this->render_widget_option_selected( 'type', $args['type'], 'taxonomy', $is_template ); ?>>
998                            <?php esc_html_e( 'Taxonomy', 'jetpack-search-pkg' ); ?>
999                        </option>
1000                        <option value="post_type" <?php $this->render_widget_option_selected( 'type', $args['type'], 'post_type', $is_template ); ?>>
1001                            <?php esc_html_e( 'Post Type', 'jetpack-search-pkg' ); ?>
1002                        </option>
1003                        <?php if ( $is_instant_search ) : ?>
1004                        <option value="author" <?php $this->render_widget_option_selected( 'type', $args['type'], 'author', $is_template ); ?>>
1005                            <?php esc_html_e( 'Author', 'jetpack-search-pkg' ); ?>
1006                        </option>
1007                        <option value="blog_id" <?php $this->render_widget_option_selected( 'type', $args['type'], 'blog_id', $is_template ); ?>>
1008                            <?php esc_html_e( 'Blog', 'jetpack-search-pkg' ); ?>
1009                        </option>
1010                        <?php endif; ?>
1011                        <option value="date_histogram" <?php $this->render_widget_option_selected( 'type', $args['type'], 'date_histogram', $is_template ); ?>>
1012                            <?php esc_html_e( 'Date', 'jetpack-search-pkg' ); ?>
1013                        </option>
1014                        <option value="product_attribute" <?php $this->render_widget_option_selected( 'type', $args['type'], 'product_attribute', $is_template ); ?>>
1015                            <?php esc_html_e( 'Product Attributes', 'jetpack-search-pkg' ); ?>
1016                        </option>
1017                    </select>
1018                </label>
1019            </p>
1020
1021            <p class="jetpack-search-filters-widget__taxonomy-select">
1022                <label>
1023                    <?php
1024                        esc_html_e( 'Choose a taxonomy:', 'jetpack-search-pkg' );
1025                        $seen_taxonomy_labels = array();
1026                    ?>
1027                    <select name="<?php echo esc_attr( $this->get_field_name( 'taxonomy_type' ) ); ?>[]" class="widefat taxonomy-select">
1028                        <?php foreach ( $this->get_allowed_taxonomies_for_widget_filters() as $taxonomy ) : ?>
1029                            <option value="<?php echo esc_attr( $taxonomy->name ); ?><?php $this->render_widget_option_selected( 'taxonomy', $args['taxonomy'], $taxonomy->name, $is_template ); ?>>
1030                                <?php
1031                                    $label = in_array( $taxonomy->label, $seen_taxonomy_labels, true )
1032                                        ? sprintf(
1033                                            /* translators: %1$s is the taxonomy name, %2s is the name of its type to help distinguish between several taxonomies with the same name, e.g. category and tag. */
1034                                            _x( '%1$s (%2$s)', 'A label for a taxonomy selector option', 'jetpack-search-pkg' ),
1035                                            $taxonomy->label,
1036                                            $taxonomy->name
1037                                        )
1038                                        : $taxonomy->label;
1039                                    echo esc_html( $label );
1040                                    $seen_taxonomy_labels[] = $taxonomy->label;
1041                                ?>
1042                            </option>
1043                        <?php endforeach; ?>
1044                    </select>
1045                </label>
1046            </p>
1047
1048            <p class="jetpack-search-filters-widget__date-histogram-select">
1049                <label>
1050                    <?php esc_html_e( 'Choose a field:', 'jetpack-search-pkg' ); ?>
1051                    <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_field' ) ); ?>[]" class="widefat date-field-select">
1052                        <option value="post_date" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date', $is_template ); ?>>
1053                            <?php esc_html_e( 'Date', 'jetpack-search-pkg' ); ?>
1054                        </option>
1055                        <option value="post_date_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_date_gmt', $is_template ); ?>>
1056                            <?php esc_html_e( 'Date GMT', 'jetpack-search-pkg' ); ?>
1057                        </option>
1058                        <option value="post_modified" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified', $is_template ); ?>>
1059                            <?php esc_html_e( 'Modified', 'jetpack-search-pkg' ); ?>
1060                        </option>
1061                        <option value="post_modified_gmt" <?php $this->render_widget_option_selected( 'field', $args['field'], 'post_modified_gmt', $is_template ); ?>>
1062                            <?php esc_html_e( 'Modified GMT', 'jetpack-search-pkg' ); ?>
1063                        </option>
1064                    </select>
1065                </label>
1066            </p>
1067
1068            <p class="jetpack-search-filters-widget__date-histogram-select">
1069                <label>
1070                    <?php esc_html_e( 'Choose an interval:', 'jetpack-search-pkg' ); ?>
1071                    <select name="<?php echo esc_attr( $this->get_field_name( 'date_histogram_interval' ) ); ?>[]" class="widefat date-interval-select">
1072                        <option value="month" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'month', $is_template ); ?>>
1073                            <?php esc_html_e( 'Month', 'jetpack-search-pkg' ); ?>
1074                        </option>
1075                        <option value="year" <?php $this->render_widget_option_selected( 'interval', $args['interval'], 'year', $is_template ); ?>>
1076                            <?php esc_html_e( 'Year', 'jetpack-search-pkg' ); ?>
1077                        </option>
1078                    </select>
1079                </label>
1080            </p>
1081
1082            <p class="jetpack-search-filters-widget__title">
1083                <label>
1084                    <?php esc_html_e( 'Title:', 'jetpack-search-pkg' ); ?>
1085                    <input
1086                        class="widefat"
1087                        type="text"
1088                        name="<?php echo esc_attr( $this->get_field_name( 'filter_name' ) ); ?>[]"
1089                        value="<?php $this->render_widget_attr( 'name', $args['name'], $is_template ); ?>"
1090                        placeholder="<?php $this->render_widget_attr( 'name_placeholder', $args['name_placeholder'], $is_template ); ?>"
1091                    />
1092                </label>
1093            </p>
1094
1095            <div class="jetpack-search-filters-widget__product-attribute-inclusions">
1096            <?php
1097            if ( function_exists( 'wc_get_attribute_taxonomies' ) && function_exists( 'wc_attribute_taxonomy_name' ) ) {
1098                $product_attributes  = wc_get_attribute_taxonomies();
1099                $included_attributes = ! $is_template && isset( $args['included_attributes'] ) ? (array) $args['included_attributes'] : array();
1100
1101                if ( ! empty( $product_attributes ) ) :
1102                    ?>
1103                    <p>
1104                        <label><?php esc_html_e( 'Select which attributes to display as filters. Leave blank to show all.', 'jetpack-search-pkg' ); ?></label>
1105                    </p>
1106                    <div class="jetpack-search-filters-widget__attribute-checkboxes">
1107                        <?php
1108                        foreach ( $product_attributes as $attribute ) :
1109                            $attribute_name = wc_attribute_taxonomy_name( $attribute->attribute_name );
1110                            $is_included    = in_array( $attribute_name, $included_attributes, true );
1111                            ?>
1112                            <label class="jetpack-search-filters-widget__attribute-checkbox">
1113                                <input
1114                                    type="checkbox"
1115                                    name="<?php echo esc_attr( $this->get_field_name( 'included_attributes_' . ( $is_template ? '<%= data.index %>' : $filter_index ) ) ); ?>[]"
1116                                    value="<?php echo esc_attr( $attribute_name ); ?>"
1117                                    <?php checked( $is_included ); ?>
1118                                />
1119                                <?php echo esc_html( $attribute->attribute_label ); ?>
1120                            </label>
1121                        <?php endforeach; ?>
1122                    </div>
1123                    <?php
1124                    endif;
1125            }
1126            ?>
1127            </div>
1128
1129            <p class="jetpack-search-filters-widget__filter-count">
1130                <label>
1131                    <?php esc_html_e( 'Maximum number of filters (1-50):', 'jetpack-search-pkg' ); ?>
1132                    <input
1133                        class="widefat filter-count"
1134                        name="<?php echo esc_attr( $this->get_field_name( 'num_filters' ) ); ?>[]"
1135                        type="number"
1136                        value="<?php $this->render_widget_attr( 'count', $args['count'], $is_template ); ?>"
1137                        min="1"
1138                        max="50"
1139                        step="1"
1140                        required
1141                    />
1142                </label>
1143            </p>
1144
1145            <p class="jetpack-search-filters-widget__controls">
1146                <a href="#" class="delete"><?php esc_html_e( 'Remove', 'jetpack-search-pkg' ); ?></a>
1147            </p>
1148        </div>
1149            <?php
1150    }
1151
1152    /**
1153     * Returns the taxonomies for search widget taxonomy dropdown.
1154     */
1155    protected function get_allowed_taxonomies_for_widget_filters() {
1156        return array_filter(
1157            get_taxonomies( array( 'public' => true ), 'objects' ),
1158            function ( $taxonomy ) {
1159                return in_array(
1160                    $taxonomy->name,
1161                    /**
1162                     * Filters the taxonomies that shows in filter drop downs of the search widget.
1163                     *
1164                     * @since  0.16.0
1165                     *
1166                     * @param array $taxonomies_to_show List of taxonomies that shown for search widget.
1167                     */
1168                    apply_filters(
1169                        'jetpack_search_allowed_taxonomies_for_widget_filters',
1170                        array_merge( array( 'category', 'post_tag' ), Search_Sync_Module::get_all_taxonomies() )
1171                    ),
1172                    true
1173                );
1174            }
1175        );
1176    }
1177}