PluginProbe
ElasticPress / 4.2.0
ElasticPress v4.2.0
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.0, at includes/classes/Feature/Facets/Facets.php

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