PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-search / src / widgets / class-search-widget.php

class-search-widget.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-search/src/widgets/class-search-widget.php

1,178 lines 41.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Jetpack Search widget.
4 *
5 * @package automattic/jetpack-search
6 */
7
8 namespace Automattic\Jetpack\Search;
9
10 use Automattic\Jetpack\Assets;
11 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 use Automattic\Jetpack\Redirect;
13 use Automattic\Jetpack\Status;
14 use Automattic\Jetpack\Sync\Modules\Search as Search_Sync_Module;
15 use Automattic\Jetpack\Tracking;
16
17 if ( ! 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 */
28 class 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 = $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 $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 = $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 ) {
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'][] = $filter['type'] ?? '';
736 $instance['taxonomy_type'][] = $filter['taxonomy'] ?? '';
737 $instance['filter_name'][] = $filter['name'] ?? '';
738 $instance['num_filters'][] = $filter['count'] ?? 5;
739 $instance['date_histogram_field'][] = $filter['field'] ?? '';
740 $instance['date_histogram_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 }
1178