PluginProbe
ElasticPress / 4.4.0
ElasticPress v4.4.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 / utils.php

utils.php in ElasticPress 4.4.0, at includes/utils.php

692 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ElasticPress utility functions
4 *
5 * @since 3.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Utils;
10
11 use ElasticPress\IndexHelper;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * Retrieve the EPIO subscription credentials.
19 *
20 * @since 2.5
21 * @return array
22 */
23 function get_epio_credentials() {
24 if ( defined( 'EP_CREDENTIALS' ) && EP_CREDENTIALS ) {
25 $raw_credentials = explode( ':', EP_CREDENTIALS );
26 if ( is_array( $raw_credentials ) && 2 === count( $raw_credentials ) ) {
27 $credentials = array(
28 'username' => $raw_credentials[0],
29 'token' => $raw_credentials[1],
30 );
31 }
32 $credentials = sanitize_credentials( $credentials );
33 } elseif ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && is_epio() ) {
34 $credentials = sanitize_credentials( get_site_option( 'ep_credentials', false ) );
35 } elseif ( is_epio() ) {
36 $credentials = sanitize_credentials( get_option( 'ep_credentials', false ) );
37 } else {
38 $credentials = [
39 'username' => '',
40 'token' => '',
41 ];
42 }
43
44 if ( ! is_array( $credentials ) ) {
45 return [
46 'username' => '',
47 'token' => '',
48 ];
49 }
50
51 return $credentials;
52 }
53
54 /**
55 * Get shield credentials
56 *
57 * @since 3.0
58 * @return string|bool
59 */
60 function get_shield_credentials() {
61 if ( defined( 'ES_SHIELD' ) && ES_SHIELD ) {
62 return ES_SHIELD;
63 } elseif ( is_epio() ) {
64 $credentials = get_epio_credentials();
65
66 return $credentials['username'] . ':' . $credentials['token'];
67 }
68
69 return false;
70 }
71
72 /**
73 * Retrieve the appropriate index prefix. Will default to EP_INDEX_PREFIX constant if it exists
74 * AKA Subscription ID.
75 *
76 * @since 2.5
77 * @return string|bool
78 */
79 function get_index_prefix() {
80 if ( defined( 'EP_INDEX_PREFIX' ) && \EP_INDEX_PREFIX ) {
81 $prefix = \EP_INDEX_PREFIX;
82 } elseif ( is_epio() ) {
83 $credentials = get_epio_credentials();
84 $prefix = $credentials['username'];
85 if (
86 ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) &&
87 ( '-' !== substr( $prefix, - 1 ) )
88 ) {
89 $prefix .= '-';
90 }
91 } else {
92 $prefix = '';
93 }
94
95 /**
96 * Filter index prefix. Defaults to nothing
97 *
98 * @since 2.5
99 * @hook ep_index_prefix
100 * @param {string} $prefix Current prefix
101 * @return {string} New prefix
102 */
103 return apply_filters( 'ep_index_prefix', $prefix );
104 }
105
106 /**
107 * Check if the host is ElasticPress.io.
108 *
109 * @since 2.6
110 * @return bool
111 */
112 function is_epio() {
113 return filter_var( preg_match( '#elasticpress\.io#i', get_host() ), FILTER_VALIDATE_BOOLEAN );
114 }
115
116 /**
117 * Determine if we should index a blog/site
118 *
119 * @param int $blog_id Blog/site id.
120 * @since 3.2
121 * @return boolean
122 */
123 function is_site_indexable( $blog_id = null ) {
124 if ( is_multisite() ) {
125 $site = get_site( $blog_id );
126
127 $is_indexable = get_blog_option( (int) $blog_id, 'ep_indexable', 'yes' );
128
129 if ( 'no' === $is_indexable || $site['deleted'] || $site['archived'] || $site['spam'] ) {
130 return false;
131 }
132 }
133
134 return true;
135 }
136
137 /**
138 * Sanitize EPIO credentials prior to storing them.
139 *
140 * @param array $credentials Array containing username and token.
141 * @since 2.6
142 * @return array
143 */
144 function sanitize_credentials( $credentials ) {
145 if ( ! is_array( $credentials ) ) {
146 return [
147 'username' => '',
148 'token' => '',
149 ];
150 }
151
152 return [
153 'username' => ( isset( $credentials['username'] ) ) ? sanitize_text_field( $credentials['username'] ) : '',
154 'token' => ( isset( $credentials['token'] ) ) ? sanitize_text_field( $credentials['token'] ) : '',
155 ];
156 }
157
158 /**
159 * Determine if ElasticPress is in the middle of an index
160 *
161 * @since 3.0
162 * @return boolean
163 */
164 function is_indexing() {
165 /**
166 * Filter whether an index is occurring in dashboard or CLI
167 *
168 * @since 3.0
169 * @hook ep_is_indexing
170 * @param {bool} $indexing True for indexing
171 * @return {bool} New indexing value
172 */
173 return apply_filters( 'ep_is_indexing', ! empty( IndexHelper::factory()->get_index_meta() ) );
174 }
175
176 /**
177 * Check if wpcli indexing is occurring
178 *
179 * @since 3.0
180 * @return boolean
181 */
182 function is_indexing_wpcli() {
183 $index_meta = IndexHelper::factory()->get_index_meta();
184
185 /**
186 * Filter whether a CLI sync is occurring
187 *
188 * @since 3.0
189 * @hook ep_is_indexing_wpcli
190 * @param {bool} $indexing True for indexing
191 * @return {bool} New indexing value
192 */
193 return apply_filters( 'ep_is_indexing_wpcli', ( ! empty( $index_meta ) && 'cli' === $index_meta['method'] ) );
194 }
195
196 /**
197 * Retrieve the appropriate host. Will default to EP_HOST constant if it exists
198 *
199 * @since 2.1
200 * @return string|bool
201 */
202 function get_host() {
203
204 if ( defined( 'EP_HOST' ) && EP_HOST ) {
205 $host = EP_HOST;
206 } else {
207 $host = get_option( 'ep_host', false );
208 }
209
210 /**
211 * Filter ElasticPress host to use
212 *
213 * @since 2.1
214 * @hook ep_host
215 * @param {string} $host Current EP host
216 * @return {string} Host to use
217 */
218 return apply_filters( 'ep_host', $host );
219 }
220
221 /**
222 * Get a site. Wraps get_site for formatting purposes
223 *
224 * @param int $site_id Site/blog id
225 * @since 3.2
226 * @return array
227 */
228 function get_site( $site_id ) {
229 $site = \get_site( $site_id );
230
231 return [
232 'blog_id' => $site->blog_id,
233 'domain' => $site->domain,
234 'path' => $site->path,
235 'site_id' => $site->site_id,
236 'deleted' => $site->deleted,
237 'archived' => $site->archived,
238 'spam' => $site->spam,
239 ];
240 }
241
242 /**
243 * Wrapper function for get_sites - allows us to have one central place for the `ep_indexable_sites` filter
244 *
245 * @param int $limit The maximum amount of sites retrieved, Use 0 to return all sites.
246 * @since 3.0
247 * @return array
248 */
249 function get_sites( $limit = 0 ) {
250
251 if ( ! is_multisite() ) {
252 return [];
253 }
254
255 /**
256 * Filter arguments to use to query for sites on network
257 *
258 * @since 2.1
259 * @hook ep_indexable_sites_args
260 * @param {array} $args Array of args to query sites with. See WP_Site_Query
261 * @return {array} New arguments
262 */
263 $args = apply_filters(
264 'ep_indexable_sites_args',
265 array(
266 'limit' => $limit,
267 'number' => $limit,
268 )
269 );
270
271 $site_objects = \get_sites( $args );
272 $sites = [];
273
274 foreach ( $site_objects as $site ) {
275 $sites[] = get_site( $site->blog_id );
276 }
277
278 /**
279 * Filter indexable sites
280 *
281 * @since 3.0
282 * @hook ep_indexable_sites
283 * @param {array} $sites Current sites. Instances of WP_Site
284 * @return {array} New array of sites
285 */
286 return apply_filters( 'ep_indexable_sites', $sites );
287 }
288
289 /**
290 * Whether plugin is network activated
291 *
292 * Determines whether plugin is network activated or just on the local site.
293 *
294 * @since 3.0
295 * @param string $plugin the plugin base name.
296 * @return bool True if network activated or false.
297 */
298 function is_network_activated( $plugin ) {
299
300 $plugins = get_site_option( 'active_sitewide_plugins' );
301
302 if ( is_multisite() && isset( $plugins[ $plugin ] ) ) {
303 return true;
304 }
305
306 return false;
307 }
308
309
310 /**
311 * Performant utility function for building a term tree.
312 *
313 * Tree will look like this:
314 * [
315 * WP_Term(
316 * name
317 * slug
318 * children ->[
319 * WP_Term()
320 * ]
321 * ),
322 * WP_Term()
323 * ]
324 *
325 * @param array $all_terms Pass get_terms() as this argument where terms are objects NOT arrays.
326 * @param string|bool $orderby Can be count|name|false. This is how each tree branch will be ordered.
327 * @param string $order Can be asc|desc. This is the direction ordering will occur.
328 * @param bool $flat If false, a tree will be returned e.g. an array of top level terms
329 * which children linked within each node. If true, the tree will be
330 * "flattened".
331 * @since 2.5
332 * @return array
333 */
334 function get_term_tree( $all_terms, $orderby = 'count', $order = 'desc', $flat = false ) {
335 $terms_map = [];
336 $terms_tree = [];
337 $iteration_id = 0;
338
339 while ( true ) {
340 if ( empty( $all_terms ) ) {
341 break;
342 }
343
344 foreach ( $all_terms as $key => $term ) {
345 $iteration_id++;
346
347 if ( ! isset( $term->children ) ) {
348 $term->children = [];
349 }
350
351 if ( ! isset( $terms_map[ $term->term_id ] ) ) {
352 $terms_map[ $term->term_id ] = $term;
353 }
354
355 $parent_term = get_term( $term->parent, $term->taxonomy );
356
357 if ( empty( $term->parent ) || is_wp_error( $parent_term ) || ! $parent_term ) {
358 $term->level = 0;
359
360 if ( empty( $orderby ) ) {
361 $terms_tree[] = $term;
362 } elseif ( 'count' === $orderby ) {
363 /**
364 * We add this weird number to get past terms with the same count
365 */
366 $terms_tree[ ( ( $term->count * 10000000 ) + $iteration_id ) ] = $term;
367 } elseif ( 'name' === $orderby ) {
368 $terms_tree[ strtolower( $term->name ) ] = $term;
369 }
370
371 unset( $all_terms[ $key ] );
372 } else {
373 if ( ! empty( $terms_map[ $term->parent ] ) && isset( $terms_map[ $term->parent ]->level ) ) {
374
375 if ( empty( $orderby ) ) {
376 $terms_map[ $term->parent ]->children[] = $term;
377 } elseif ( 'count' === $orderby ) {
378 $terms_map[ $term->parent ]->children[ ( ( $term->count * 10000000 ) + $iteration_id ) ] = $term;
379 } elseif ( 'name' === $orderby ) {
380 $terms_map[ $term->parent ]->children[ $term->name ] = $term;
381 }
382
383 $parent_level = ( $terms_map[ $term->parent ]->level ) ? $terms_map[ $term->parent ]->level : 0;
384
385 $term->level = $parent_level + 1;
386 $term->parent_term = $terms_map[ $term->parent ];
387
388 unset( $all_terms[ $key ] );
389 }
390 }
391 }
392 }
393
394 if ( ! empty( $orderby ) ) {
395 if ( 'asc' === $order ) {
396 ksort( $terms_tree );
397 } else {
398 krsort( $terms_tree );
399 }
400
401 foreach ( $terms_map as $term ) {
402 if ( 'asc' === $order ) {
403 ksort( $term->children );
404 } else {
405 krsort( $term->children );
406 }
407
408 $term->children = array_values( $term->children );
409 }
410
411 $terms_tree = array_values( $terms_tree );
412 }
413
414 if ( $flat ) {
415 $flat_tree = [];
416
417 foreach ( $terms_tree as $term ) {
418 $flat_tree[] = $term;
419 $to_process = $term->children;
420 while ( ! empty( $to_process ) ) {
421 $term = array_shift( $to_process );
422 $flat_tree[] = $term;
423
424 if ( ! empty( $term->children ) ) {
425 $to_process = array_merge( $term->children, $to_process );
426 }
427 }
428 }
429
430 return $flat_tree;
431 }
432
433 return $terms_tree;
434 }
435
436 /**
437 * Returns the defaiult language for ES mapping.
438 *
439 * @return string Default EP language.
440 */
441 function get_language() {
442 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
443 $ep_language = get_site_option( 'ep_language' );
444 } else {
445 $ep_language = get_option( 'ep_language' );
446 }
447
448 $ep_language = ! empty( $ep_language ) ? $ep_language : get_locale();
449
450 /**
451 * Filter the default language to use at index time
452 *
453 * @since 3.1
454 * @param {string} The current language.
455 * @hook ep_default_language
456 * @return {string} New language
457 */
458 return apply_filters( 'ep_default_language', $ep_language );
459 }
460
461 /**
462 * Returns the status of an ongoing index operation.
463 *
464 * Returns the status of an ongoing index operation in array with the following fields:
465 * indexing | boolean | True if index operation is ongoing or false
466 * method | string | 'cli', 'web' or 'none'
467 * items_indexed | integer | Total number of items indexed
468 * total_items | integer | Total number of items indexed or -1 if not yet determined
469 * slug | string | The slug of the indexable
470 *
471 * @since 3.5.2
472 * @return array|boolean
473 */
474 function get_indexing_status() {
475
476 $index_status = false;
477
478 $index_meta = IndexHelper::factory()->get_index_meta();
479
480 if ( ! empty( $index_meta ) ) {
481 $index_status = $index_meta;
482
483 $index_status['indexing'] = true;
484
485 if ( ! empty( $index_meta['current_sync_item'] ) ) {
486 $index_status['items_indexed'] = $index_meta['current_sync_item']['synced'];
487 $index_status['url'] = $index_meta['current_sync_item']['url'] ?? ''; // Global indexables won't have a url.
488 $index_status['total_items'] = $index_meta['current_sync_item']['total'];
489 $index_status['slug'] = $index_meta['current_sync_item']['indexable'];
490 }
491
492 // Change method name for retrocompatibility.
493 // `dashboard` is used mainly because hooks names depend on that.
494 if ( ! empty( $index_status['method'] ) && 'dashboard' === $index_status['method'] ) {
495 $index_status['method'] = 'web';
496 }
497
498 if ( ! empty( $index_status['method'] ) && 'web' === $index_status['method'] ) {
499 $should_interrupt_sync = filter_var(
500 get_transient( 'ep_sync_interrupted' ),
501 FILTER_VALIDATE_BOOLEAN
502 );
503
504 $index_status['should_interrupt_sync'] = $should_interrupt_sync;
505 }
506 }
507
508 return $index_status;
509
510 }
511
512 /**
513 * Use the correct update option function depending on the context (multisite or not)
514 *
515 * @since 3.6.0
516 * @param string $option Name of the option to update.
517 * @param mixed $value Option value.
518 * @param mixed $autoload Whether to load the option when WordPress starts up.
519 * @return bool
520 */
521 function update_option( $option, $value, $autoload = null ) {
522 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
523 return \update_site_option( $option, $value );
524 }
525 return \update_option( $option, $value, $autoload );
526 }
527
528 /**
529 * Use the correct get option function depending on the context (multisite or not)
530 *
531 * @since 3.6.0
532 * @param string $option Name of the option to get.
533 * @param mixed $default_value Default value.
534 * @return bool
535 */
536 function get_option( $option, $default_value = false ) {
537 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
538 return \get_site_option( $option, $default_value );
539 }
540 return \get_option( $option, $default_value );
541 }
542
543 /**
544 * Use the correct delete option function depending on the context (multisite or not)
545 *
546 * @since 3.6.0
547 * @param string $option Name of the option to delete.
548 * @return bool
549 */
550 function delete_option( $option ) {
551 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
552 return \delete_site_option( $option );
553 }
554 return \delete_option( $option );
555 }
556
557 /**
558 * Check if queries for the current request are going to be integrated with
559 * ElasticPress.
560 *
561 * Public requests and REST API requests are integrated by default, but admin
562 * requests will only be integrated in if the `ep_admin_wp_query_integration`
563 * filter returns `true`, and and admin-ajax.php requests will only be
564 * integrated if the `ep_ajax_wp_query_integration` filter returns `true`.
565 *
566 * If specific types of requests are passed, true will only be returned if the
567 * current request also matches one of the passed types.
568 *
569 * This function is used by features to determine whether they should hook into
570 * the current request.
571 *
572 * @param string $context Slug of the feature that is performing the check.
573 * Passed to the `ep_is_integrated_request` filter.
574 * @param string[] $types Which types of request to check. Any of 'admin',
575 * 'ajax', 'public', and 'rest'. Defaults to all
576 * types.
577 * @return bool Whether the current request supports ElasticPress integration
578 * and is of a given type.
579 *
580 * @since 3.6.0
581 */
582 function is_integrated_request( $context, $types = [] ) {
583 if ( empty( $types ) ) {
584 $types = [ 'admin', 'ajax', 'public', 'rest' ];
585 }
586
587 $is_admin_request = is_admin();
588 $is_ajax_request = defined( 'DOING_AJAX' ) && DOING_AJAX;
589 $is_rest_request = defined( 'REST_REQUEST' ) && REST_REQUEST;
590 $is_integrated_admin_request = false;
591 $is_integrated_ajax_request = false;
592 $is_integrated_public_request = false;
593 $is_integrated_rest_request = false;
594
595 if ( $is_admin_request && ! $is_ajax_request && in_array( 'admin', $types, true ) ) {
596
597 /**
598 * Filter whether to integrate with admin queries.
599 *
600 * @hook ep_admin_wp_query_integration
601 * @param bool $integrate True to integrate.
602 * @return bool New value.
603 */
604 $is_integrated_admin_request = apply_filters( 'ep_admin_wp_query_integration', false );
605 }
606
607 if ( $is_ajax_request && in_array( 'ajax', $types, true ) ) {
608
609 /**
610 * Filter to integrate with admin ajax queries.
611 *
612 * @hook ep_ajax_wp_query_integration
613 * @param bool $integrate True to integrate.
614 * @return bool New value.
615 */
616 $is_integrated_ajax_request = apply_filters( 'ep_ajax_wp_query_integration', false );
617 }
618
619 if ( $is_rest_request && in_array( 'rest', $types, true ) ) {
620 $is_integrated_rest_request = true;
621 }
622
623 if ( ! $is_admin_request && ! $is_ajax_request && ! $is_rest_request && in_array( 'public', $types, true ) ) {
624 $is_integrated_public_request = true;
625 }
626
627 /**
628 * Is the current request any of the supported requests.
629 */
630 $is_integrated = (
631 $is_integrated_admin_request ||
632 $is_integrated_ajax_request ||
633 $is_integrated_public_request ||
634 $is_integrated_rest_request
635 );
636
637 /**
638 * Filter whether the queries for the current request should be integrated.
639 *
640 * @hook ep_is_integrated_request
641 * @param bool $is_integrated Whether queries for the request will be
642 * integrated.
643 * @param string $context Context for the original check. Usually the
644 * slug of the feature doing the check.
645 * @param array $types Which requests types are being checked.
646 * @return bool Whether queries for the request will be integrated.
647 *
648 * @since 3.6.2
649 */
650 return apply_filters( 'ep_is_integrated_request', $is_integrated, $context, $types );
651 }
652
653 /**
654 * Get asset info from extracted asset files
655 *
656 * @param string $slug Asset slug as defined in build/webpack configuration
657 * @param string $attribute Optional attribute to get. Can be version or dependencies
658 * @return string|array
659 */
660 function get_asset_info( $slug, $attribute = null ) {
661 if ( file_exists( EP_PATH . 'dist/js/' . $slug . '.asset.php' ) ) {
662 $asset = require EP_PATH . 'dist/js/' . $slug . '.asset.php';
663 } elseif ( file_exists( EP_PATH . 'dist/css/' . $slug . '.asset.php' ) ) {
664 $asset = require EP_PATH . 'dist/css/' . $slug . '.asset.php';
665 } else {
666 return null;
667 }
668
669 if ( ! empty( $attribute ) && isset( $asset[ $attribute ] ) ) {
670 return $asset[ $attribute ];
671 }
672
673 return $asset;
674 }
675
676 /**
677 * Return the Sync Page URL.
678 *
679 * @since 4.4.0
680 * @param boolean $do_sync Whether the link should or should not start a resync.
681 * @return string
682 */
683 function get_sync_url( bool $do_sync = false ) : string {
684 $page = 'admin.php?page=elasticpress-sync';
685 if ( $do_sync ) {
686 $page .= '&do_sync';
687 }
688 return ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ?
689 network_admin_url( $page ) :
690 admin_url( $page );
691 }
692