PluginProbe
ElasticPress / 4.2.2
ElasticPress v4.2.2
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / Facets / Facets.php

Facets.php in ElasticPress 4.2.2, at includes/classes/Feature/Facets/Facets.php

573 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Facets feature
4 *
5 * @since 2.5
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\Facets;
10
11 use ElasticPress\Feature as Feature;
12 use ElasticPress\Features as Features;
13 use ElasticPress\Utils as Utils;
14 use ElasticPress\FeatureRequirementsStatus as FeatureRequirementsStatus;
15 use ElasticPress\Indexables as Indexables;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Facets feature class
23 */
24 class Facets extends Feature {
25
26 /**
27 * Block instance.
28 *
29 * @since 4.2.0
30 * @var Block
31 */
32 public $block;
33
34 /**
35 * Initialize feature setting it's config
36 *
37 * @since 3.0
38 */
39 public function __construct() {
40 $this->slug = 'facets';
41
42 $this->title = esc_html__( 'Facets', 'elasticpress' );
43
44 $this->summary = __( 'Add controls to your website to filter content by one or more taxonomies.', 'elasticpress' );
45
46 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#facets', 'elasticpress' );
47
48 $this->requires_install_reindex = false;
49
50 $this->default_settings = [
51 'match_type' => 'all',
52 ];
53
54 parent::__construct();
55 }
56
57 /**
58 * Setup hooks and filters for feature
59 *
60 * @since 2.5
61 */
62 public function setup() {
63 global $pagenow;
64
65 // This feature should not run while in the editor.
66 if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) ) {
67 return;
68 }
69
70 add_action( 'widgets_init', [ $this, 'register_widgets' ] );
71 add_action( 'ep_valid_response', [ $this, 'get_aggs' ], 10, 4 );
72 add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
73 add_action( 'pre_get_posts', [ $this, 'facet_query' ] );
74 add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
75 add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
76 add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 );
77
78 $this->block = new Block();
79 $this->block->setup();
80 }
81
82 /**
83 * Dashboard facet settings
84 *
85 * @since 2.5
86 */
87 public function output_feature_box_settings() {
88 $settings = $this->get_settings();
89
90 if ( ! $settings ) {
91 $settings = [];
92 }
93
94 $settings = wp_parse_args( $settings, $this->default_settings );
95 ?>
96 <div class="field">
97 <div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
98 <div class="input-wrap">
99 <label><input name="settings[match_type]" type="radio" <?php checked( $settings['match_type'], 'all' ); ?> value="all"><?php echo wp_kses_post( __( 'Show any content tagged to <strong>all</strong> selected terms', 'elasticpress' ) ); ?></label><br>
100 <label><input name="settings[match_type]" type="radio" <?php checked( $settings['match_type'], 'any' ); ?> value="any"><?php echo wp_kses_post( __( 'Show all content tagged to <strong>any</strong> selected term', 'elasticpress' ) ); ?></label>
101 <p class="field-description"><?php esc_html_e( '"All" will only show content that matches all facets. "Any" will show content that matches any facet.', 'elasticpress' ); ?></p>
102 </div>
103 </div>
104 <?php
105 }
106
107 /**
108 * If we are doing or matches, we need to remove filters from aggs
109 *
110 * @param array $args ES arguments
111 * @param array $query_args Query arguments
112 * @param WP_Query $query WP Query instance
113 * @since 2.5
114 * @return array
115 */
116 public function set_agg_filters( $args, $query_args, $query ) {
117 if ( empty( $query_args['ep_facet'] ) ) {
118 return $args;
119 }
120
121 // @todo For some reason these are appearing in the query args, need to investigate
122 unset( $query_args['category_name'] );
123 unset( $query_args['cat'] );
124 unset( $query_args['tag'] );
125 unset( $query_args['tag_id'] );
126 unset( $query_args['taxonomy'] );
127 unset( $query_args['term'] );
128
129 $facet_query_args = $query_args;
130
131 $settings = $this->get_settings();
132
133 $settings = wp_parse_args(
134 $settings,
135 array(
136 'match_type' => 'all',
137 )
138 );
139
140 if ( ! empty( $facet_query_args['tax_query'] ) ) {
141 remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
142
143 foreach ( $facet_query_args['tax_query'] as $key => $taxonomy ) {
144 if ( is_array( $taxonomy ) ) {
145 if ( 'any' === $settings['match_type'] ) {
146 unset( $facet_query_args['tax_query'][ $key ] );
147 }
148 }
149 }
150
151 $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $facet_query_args, $query );
152
153 $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter'];
154
155 add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
156 }
157
158 return $args;
159 }
160
161 /**
162 * Output scripts for widget admin
163 *
164 * @param string $hook WP hook
165 * @since 2.5
166 */
167 public function admin_scripts( $hook ) {
168 if ( 'widgets.php' !== $hook ) {
169 return;
170 }
171
172 wp_enqueue_style(
173 'elasticpress-facets-admin',
174 EP_URL . 'dist/css/facets-admin-styles.min.css',
175 Utils\get_asset_info( 'facets-admin-styles', 'dependencies' ),
176 Utils\get_asset_info( 'facets-admin-styles', 'version' )
177 );
178 }
179
180 /**
181 * Output front end facets styles
182 *
183 * @since 2.5
184 */
185 public function front_scripts() {
186 wp_register_script(
187 'elasticpress-facets',
188 EP_URL . 'dist/js/facets-script.min.js',
189 Utils\get_asset_info( 'facets-script', 'dependencies' ),
190 Utils\get_asset_info( 'facets-script', 'version' ),
191 true
192 );
193
194 wp_register_style(
195 'elasticpress-facets',
196 EP_URL . 'dist/css/facets-styles.min.css',
197 Utils\get_asset_info( 'facets-styles', 'dependencies' ),
198 Utils\get_asset_info( 'facets-styles', 'version' )
199 );
200 }
201
202 /**
203 * Figure out if we can/should facet the query
204 *
205 * @param WP_Query $query WP Query
206 * @since 2.5
207 * @return bool
208 */
209 public function is_facetable( $query ) {
210
211 /**
212 * Bypass the standard checks and set a query to be facetable
213 *
214 * @hook ep_is_facetable
215 * @param {bool} $bypass Defaults to false.
216 * @param {WP_Query} $query The current WP_Query.
217 * @return {bool} true to bypass, false to ignore
218 */
219 if ( \apply_filters( 'ep_is_facetable', false, $query ) ) {
220 return true;
221 }
222
223 if ( is_admin() ) {
224 return false;
225 }
226
227 if ( defined( 'WP_CLI' ) && WP_CLI ) {
228 return false;
229 }
230
231 if ( ! $query->is_main_query() ) {
232 return false;
233 }
234
235 $ep_integrate = $query->get( 'ep_integrate', null );
236
237 if ( false === $ep_integrate ) {
238 return false;
239 }
240
241 $woocommerce = Features::factory()->get_registered_feature( 'woocommerce' );
242
243 if ( ! $woocommerce->is_active() && ( function_exists( 'is_product_category' ) && is_product_category() ) ) {
244 return false;
245 }
246
247 if ( ! $this->is_facetable_page( $query ) ) {
248 return false;
249 }
250
251 return true;
252 }
253
254 /**
255 * We enable ElasticPress facet on all archive/search queries as well as non-static home pages. There is no way to know
256 * when a facet widget is used before the main query is executed so we enable EP
257 * everywhere where a facet widget could be used.
258 *
259 * @param WP_Query $query WP Query
260 * @since 2.5
261 */
262 public function facet_query( $query ) {
263 if ( ! $this->is_facetable( $query ) ) {
264 return;
265 }
266
267 $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
268
269 /**
270 * Filter taxonomies made available for faceting
271 *
272 * @hook ep_facet_include_taxonomies
273 * @param {array} $taxonomies Taxonomies
274 * @return {array} New taxonomies
275 */
276 $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
277
278 if ( empty( $taxonomies ) ) {
279 return;
280 }
281
282 $query->set( 'ep_integrate', true );
283 $query->set( 'ep_facet', true );
284
285 $facets = [];
286
287 /**
288 * Retrieve aggregations based on a custom field. This field must exist on the mapping.
289 * Values available out-of-the-box are:
290 * - slug (default)
291 * - term_id
292 * - name
293 * - parent
294 * - term_taxonomy_id
295 * - term_order
296 * - facet (retrieves a JSON representation of the term object)
297 *
298 * @since 3.6.0
299 * @hook ep_facet_use_field
300 * @param {string} $field The term field to use
301 * @return {string} The chosen term field
302 */
303 $facet_field = apply_filters( 'ep_facet_use_field', 'slug' );
304
305 foreach ( $taxonomies as $slug => $taxonomy ) {
306 $facets[ $slug ] = array(
307 'terms' => array(
308 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ),
309 'field' => 'terms.' . $slug . '.' . $facet_field,
310 ),
311 );
312 }
313
314 $aggs = array(
315 'name' => 'terms',
316 'use-filter' => true,
317 'aggs' => $facets,
318 );
319
320 $query->set( 'aggs', $aggs );
321
322 $selected_filters = $this->get_selected();
323
324 $settings = $this->get_settings();
325
326 $settings = wp_parse_args(
327 $settings,
328 array(
329 'match_type' => 'all',
330 )
331 );
332
333 $tax_query = $query->get( 'tax_query', [] );
334
335 // Account for taxonomies that should be woocommerce attributes, if WC is enabled
336 $attribute_taxonomies = [];
337 if ( function_exists( 'wc_attribute_taxonomy_name' ) ) {
338 $all_attr_taxonomies = wc_get_attribute_taxonomies();
339
340 foreach ( $all_attr_taxonomies as $attr_taxonomy ) {
341 $attribute_taxonomies[ $attr_taxonomy->attribute_name ] = wc_attribute_taxonomy_name( $attr_taxonomy->attribute_name );
342 }
343 }
344
345 foreach ( $selected_filters['taxonomies'] as $taxonomy => $filter ) {
346 $tax_query[] = [
347 'taxonomy' => isset( $attribute_taxonomies[ $taxonomy ] ) ? $attribute_taxonomies[ $taxonomy ] : $taxonomy,
348 'field' => 'slug',
349 'terms' => array_keys( $filter['terms'] ),
350 'operator' => ( 'any' === $settings['match_type'] ) ? 'or' : 'and',
351 ];
352 }
353
354 if ( ! empty( $selected_filters['taxonomies'] ) && 'any' === $settings['match_type'] ) {
355 $tax_query['relation'] = 'or';
356 }
357
358 $query->set( 'tax_query', $tax_query );
359 }
360
361 /**
362 * Hacky. Save aggregation data for later in a global
363 *
364 * @param array $response ES response
365 * @param array $query Prepared Elasticsearch query
366 * @param array $query_args Current WP Query arguments
367 * @param mixed $query_object Could be WP_Query, WP_User_Query, etc.
368 * @since 2.5
369 */
370 public function get_aggs( $response, $query, $query_args, $query_object ) {
371 if ( empty( $query_object ) || 'WP_Query' !== get_class( $query_object ) || ! $this->is_facetable( $query_object ) ) {
372 return;
373 }
374
375 $GLOBALS['ep_facet_aggs'] = false;
376
377 if ( ! empty( $response['aggregations'] ) ) {
378 $GLOBALS['ep_facet_aggs'] = [];
379
380 if ( isset( $response['aggregations']['terms'] ) && is_array( $response['aggregations']['terms'] ) ) {
381 foreach ( $response['aggregations']['terms'] as $key => $agg ) {
382 if ( 'doc_count' === $key ) {
383 continue;
384 }
385
386 if ( ! is_array( $agg ) || empty( $agg['buckets'] ) ) {
387 continue;
388 }
389
390 $GLOBALS['ep_facet_aggs'][ $key ] = [];
391
392 foreach ( $agg['buckets'] as $bucket ) {
393 $GLOBALS['ep_facet_aggs'][ $key ][ $bucket['key'] ] = $bucket['doc_count'];
394 }
395 }
396 }
397 }
398 }
399
400 /**
401 * Get currently selected facets from query args
402 *
403 * @since 2.5
404 * @return array
405 */
406 public function get_selected() {
407 $filters = array(
408 'taxonomies' => [],
409 );
410
411 $allowed_args = $this->get_allowed_query_args();
412 $filter_name = $this->get_filter_name();
413
414 foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification
415 if ( 0 === strpos( $key, $filter_name ) ) {
416 $taxonomy = str_replace( $filter_name, '', $key );
417
418 $filters['taxonomies'][ $taxonomy ] = array(
419 'terms' => array_fill_keys( array_map( 'trim', explode( ',', trim( $value, ',' ) ) ), true ),
420 );
421 }
422
423 if ( in_array( $key, $allowed_args, true ) ) {
424 $filters[ $key ] = $value;
425 }
426 }
427
428 return $filters;
429 }
430
431 /**
432 * Build query url
433 *
434 * @since 2.5
435 * @param array $filters Facet filters
436 * @return string
437 */
438 public function build_query_url( $filters ) {
439 $query_param = array();
440
441 if ( ! empty( $filters['taxonomies'] ) ) {
442 $tax_filters = $filters['taxonomies'];
443
444 foreach ( $tax_filters as $taxonomy => $filter ) {
445 if ( ! empty( $filter['terms'] ) ) {
446 $query_param[ $this->get_filter_name() . $taxonomy ] = implode( ',', array_keys( $filter['terms'] ) );
447 }
448 }
449 }
450
451 $allowed_args = $this->get_allowed_query_args();
452
453 if ( ! empty( $filters ) ) {
454 foreach ( $filters as $filter => $value ) {
455 if ( ! empty( $value ) && in_array( $filter, $allowed_args, true ) ) {
456 $query_param[ $filter ] = $value;
457 }
458 }
459 }
460
461 $query_string = http_build_query( $query_param );
462
463 /**
464 * Filter facet query string
465 *
466 * @hook ep_facet_query_string
467 * @param {string} $query_string Current query string
468 * @param {array} $query_param Query parameters
469 * @return {string} New query string
470 */
471 $query_string = apply_filters( 'ep_facet_query_string', $query_string, $query_param );
472
473 $url = $_SERVER['REQUEST_URI'];
474 $pagination = strpos( $url, '/page' );
475 if ( false !== $pagination ) {
476 $url = substr( $url, 0, $pagination );
477 }
478
479 return strtok( trailingslashit( $url ), '?' ) . ( ( ! empty( $query_string ) ) ? '?' . $query_string : '' );
480 }
481
482 /**
483 * Register facet widget(s)
484 *
485 * @since 2.5
486 */
487 public function register_widgets() {
488 register_widget( __NAMESPACE__ . '\Widget' );
489 }
490
491 /**
492 * Output feature box long
493 *
494 * @since 2.5
495 */
496 public function output_feature_box_long() {
497 ?>
498 <p>
499 <?php
500 // translators: URL
501 echo wp_kses_post( sprintf( __( "Adds a <a href='%s'>Facet widget</a> that administrators can add to the website's sidebars (widgetized areas), so that visitors can filter applicable content and search results by one or more taxonomy terms.", 'elasticpress' ), esc_url( admin_url( 'widgets.php' ) ) ) );
502 ?>
503 </p>
504 <?php
505 }
506
507 /**
508 * Returns allowed query args for facets
509 *
510 * @return mixed|void
511 * @since 3.6.0
512 */
513 public function get_allowed_query_args() {
514 $args = array( 's', 'post_type' );
515
516 /**
517 * Filter allowed query args
518 *
519 * @hook ep_facet_allowed_query_args
520 * @since 3.6.0
521 * @param {array} $args Post types
522 * @return {array} New post types
523 */
524 return apply_filters( 'ep_facet_allowed_query_args', $args );
525 }
526
527 /**
528 * Get the facet filter name.
529 *
530 * @return string The filter name.
531 */
532 protected function get_filter_name() {
533 /**
534 * Filter the facet filter name that's added to the URL
535 *
536 * @hook ep_facet_filter_name
537 * @since 4.0.0
538 * @param {string} Facet filter name
539 * @return {string} New facet filter name
540 */
541 return apply_filters( 'ep_facet_filter_name', 'ep_filter_' );
542 }
543
544 /**
545 * Get all taxonomies that could be selected for a facet.
546 *
547 * @since 4.2.0
548 * @return array
549 */
550 public function get_facetable_taxonomies() {
551 $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
552 /**
553 * Filter taxonomies made available for faceting
554 *
555 * @hook ep_facet_include_taxonomies
556 * @param {array} $taxonomies Taxonomies
557 * @return {array} New taxonomies
558 */
559 return apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
560 }
561
562 /**
563 * Figure out if Facet widget can display on page.
564 *
565 * @param WP_Query $query WP Query
566 * @since 4.2.1
567 * @return bool
568 */
569 protected function is_facetable_page( $query ) {
570 return $query->is_home() || $query->is_search() || $query->is_tax() || $query->is_tag() || $query->is_category() || $query->is_post_type_archive();
571 }
572 }
573