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