PluginProbe
ElasticPress / trunk
ElasticPress vtrunk
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 5.1.0 All 107 releases
elasticpress / includes / utils.php

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

980 lines 26.5 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 WP capability needed for a user to interact with ElasticPress in the admin
56 *
57 * @since 4.5.0, 5.1.0 added $context
58 * @param string $context Context for the capability. Defaults to empty string.
59 * @return string
60 */
61 function get_capability( string $context = '' ): string {
62 /**
63 * Filter the WP capability needed to interact with ElasticPress in the admin
64 *
65 * Example:
66 * ```
67 * add_filter(
68 * 'ep_capability',
69 * function ( $cacapability, $context ) {
70 * return ( 'synonyms' === $context ) ?
71 * 'manage_elasticpress_synonyms' :
72 * $cacapability;
73 * },
74 * 10,
75 * 2
76 * );
77 * ```
78 *
79 * @since 4.5.0, 5.1.0 added $context
80 * @hook ep_capability
81 * @param {string} $capability Capability name. Defaults to `'manage_elasticpress'`
82 * @param {string} $context Additional context
83 * @return {string} New capability value
84 */
85 return apply_filters( 'ep_capability', 'manage_elasticpress', $context );
86 }
87
88 /**
89 * Get WP capability needed for a user to interact with ElasticPress in the network admin
90 *
91 * @since 4.5.0, 5.1.0 added $context
92 * @param string $context Context for the capability. Defaults to empty string.
93 * @return string
94 */
95 function get_network_capability( string $context = '' ): string {
96 /**
97 * Filter the WP capability needed to interact with ElasticPress in the network admin
98 *
99 * @since 4.5.0, 5.1.0 added $context
100 * @hook ep_network_capability
101 * @param {string} $capability Capability name. Defaults to `'manage_network_elasticpress'`
102 * @param {string} $context Additional context
103 * @return {string} New capability value
104 */
105 return apply_filters( 'ep_network_capability', 'manage_network_elasticpress', $context );
106 }
107
108 /**
109 * Get mapped capabilities for post types
110 *
111 * @since 4.5.0, 5.1.0 added $context
112 * @param string $context Context for the capability. Defaults to empty string.
113 * @return array
114 */
115 function get_post_map_capabilities( string $context = '' ): array {
116 $capability = get_capability( $context );
117
118 return [
119 'edit_post' => $capability,
120 'edit_posts' => $capability,
121 'edit_others_posts' => $capability,
122 'publish_posts' => $capability,
123 'read_post' => $capability,
124 'read_private_posts' => $capability,
125 'delete_post' => $capability,
126 ];
127 }
128
129 /**
130 * Get shield credentials
131 *
132 * @since 3.0
133 * @return string|bool
134 */
135 function get_shield_credentials() {
136 if ( defined( 'ES_SHIELD' ) && ES_SHIELD ) {
137 return ES_SHIELD;
138 } elseif ( is_epio() ) {
139 $credentials = get_epio_credentials();
140
141 return $credentials['username'] . ':' . $credentials['token'];
142 }
143
144 return false;
145 }
146
147 /**
148 * Retrieve the appropriate index prefix. Will default to EP_INDEX_PREFIX constant if it exists
149 * AKA Subscription ID.
150 *
151 * @since 2.5
152 * @return string|bool
153 */
154 function get_index_prefix() {
155 if ( defined( 'EP_INDEX_PREFIX' ) && \EP_INDEX_PREFIX ) {
156 $prefix = \EP_INDEX_PREFIX;
157 } elseif ( is_epio() ) {
158 $credentials = get_epio_credentials();
159 $prefix = $credentials['username'];
160 if (
161 ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) &&
162 ( '-' !== substr( $prefix, - 1 ) )
163 ) {
164 $prefix .= '-';
165 }
166 } else {
167 $prefix = '';
168 }
169
170 /**
171 * Filter index prefix. Defaults to nothing
172 *
173 * @since 2.5
174 * @hook ep_index_prefix
175 * @param {string} $prefix Current prefix
176 * @return {string} New prefix
177 */
178 return apply_filters( 'ep_index_prefix', $prefix );
179 }
180
181 /**
182 * Check if the host is ElasticPress.io.
183 *
184 * @since 2.6
185 * @return bool
186 */
187 function is_epio() {
188 $has_url = filter_var( preg_match( '#elasticpress\.io#i', get_host() ), FILTER_VALIDATE_BOOLEAN );
189 $has_env_var = '1' === getenv( 'IS_EPIO_ENVIRONMENT' );
190 return $has_url || $has_env_var;
191 }
192
193 /**
194 * Determine if we should index a blog/site
195 *
196 * @param int $blog_id Blog/site id.
197 * @since 3.2
198 * @return boolean
199 */
200 function is_site_indexable( $blog_id = null ) {
201 if ( ! is_multisite() ) {
202 return true;
203 }
204
205 $site = get_site( $blog_id );
206
207 if ( empty( $site ) ) {
208 return false;
209 }
210
211 $is_indexable = get_site_meta( $site['blog_id'], 'ep_indexable', true );
212
213 return 'no' !== $is_indexable && ! $site['deleted'] && ! $site['archived'] && ! $site['spam'];
214 }
215
216 /**
217 * Sanitize EPIO credentials prior to storing them.
218 *
219 * @param array $credentials Array containing username and token.
220 * @since 2.6
221 * @return array
222 */
223 function sanitize_credentials( $credentials ) {
224 if ( ! is_array( $credentials ) ) {
225 return [
226 'username' => '',
227 'token' => '',
228 ];
229 }
230
231 return [
232 'username' => ( isset( $credentials['username'] ) ) ? sanitize_text_field( $credentials['username'] ) : '',
233 'token' => ( isset( $credentials['token'] ) ) ? sanitize_text_field( $credentials['token'] ) : '',
234 ];
235 }
236
237 /**
238 * Determine if ElasticPress is in the middle of an index
239 *
240 * @since 3.0
241 * @return boolean
242 */
243 function is_indexing() {
244 /**
245 * Filter whether an index is occurring in dashboard or CLI
246 *
247 * @since 3.0
248 * @hook ep_is_indexing
249 * @param {bool} $indexing True for indexing
250 * @return {bool} New indexing value
251 */
252 return apply_filters( 'ep_is_indexing', ! empty( IndexHelper::factory()->get_index_meta() ) );
253 }
254
255 /**
256 * Check if wpcli indexing is occurring
257 *
258 * @since 3.0
259 * @return boolean
260 */
261 function is_indexing_wpcli() {
262 $index_meta = IndexHelper::factory()->get_index_meta();
263
264 /**
265 * Filter whether a CLI sync is occurring
266 *
267 * @since 3.0
268 * @hook ep_is_indexing_wpcli
269 * @param {bool} $indexing True for indexing
270 * @return {bool} New indexing value
271 */
272 return apply_filters( 'ep_is_indexing_wpcli', ( ! empty( $index_meta ) && 'cli' === $index_meta['method'] ) );
273 }
274
275 /**
276 * Retrieve the appropriate host. Will default to EP_HOST constant if it exists
277 *
278 * @since 2.1
279 * @return string|bool
280 */
281 function get_host() {
282
283 if ( defined( 'EP_HOST' ) && EP_HOST ) {
284 $host = EP_HOST;
285 } else {
286 $host = get_option( 'ep_host', false );
287 }
288
289 /**
290 * Filter ElasticPress host to use
291 *
292 * @since 2.1
293 * @hook ep_host
294 * @param {string} $host Current EP host
295 * @return {string} Host to use
296 */
297 return apply_filters( 'ep_host', $host );
298 }
299
300 /**
301 * Get a site. Wraps get_site for formatting purposes
302 *
303 * @param int $site_id Site/blog id
304 * @since 3.2
305 * @return array
306 */
307 function get_site( $site_id ) {
308 $site = \get_site( $site_id );
309
310 if ( ! $site instanceof \WP_Site ) {
311 return [];
312 }
313
314 return [
315 'blog_id' => $site->blog_id,
316 'domain' => $site->domain,
317 'path' => $site->path,
318 'site_id' => $site->site_id,
319 'deleted' => $site->deleted,
320 'archived' => $site->archived,
321 'spam' => $site->spam,
322 ];
323 }
324
325 /**
326 * Wrapper function for get_sites - allows us to have one central place for the `ep_indexable_sites` filter
327 *
328 * @param int $limit The maximum amount of sites retrieved, Use 0 to return all sites.
329 * @param bool $only_indexable Whether should be returned only indexable sites or not.
330 * @since 3.0, 4.7.0 added `$only_indexable`
331 * @return array
332 */
333 function get_sites( $limit = 0, $only_indexable = false ) {
334 if ( ! is_multisite() ) {
335 return [];
336 }
337
338 $args = [
339 'limit' => $limit,
340 'number' => $limit,
341 ];
342
343 if ( $only_indexable ) {
344 $args = array_merge(
345 $args,
346 [
347 'spam' => 0,
348 'deleted' => 0,
349 'archived' => 0,
350 'meta_query' => [
351 'relation' => 'OR',
352 [
353 'key' => 'ep_indexable',
354 'value' => 'no',
355 'compare' => '!=',
356 ],
357 [
358 'key' => 'ep_indexable',
359 'compare' => 'NOT EXISTS',
360 ],
361 ],
362 ]
363 );
364 }
365
366 /**
367 * Filter arguments to use to query for sites on network
368 *
369 * @since 2.1
370 * @hook ep_indexable_sites_args
371 * @param {array} $args Array of args to query sites with. See WP_Site_Query
372 * @return {array} New arguments
373 */
374 $args = apply_filters( 'ep_indexable_sites_args', $args );
375
376 $site_objects = \get_sites( $args );
377 $sites = [];
378
379 foreach ( $site_objects as $site ) {
380 $sites[] = get_site( $site->blog_id );
381 }
382
383 /**
384 * Filter indexable sites
385 *
386 * @since 3.0
387 * @hook ep_indexable_sites
388 * @param {array} $sites Current sites. Instances of WP_Site
389 * @return {array} New array of sites
390 */
391 return apply_filters( 'ep_indexable_sites', $sites );
392 }
393
394 /**
395 * Whether plugin is network activated
396 *
397 * Determines whether plugin is network activated or just on the local site.
398 *
399 * @since 3.0
400 * @param string $plugin the plugin base name.
401 * @return bool True if network activated or false.
402 */
403 function is_network_activated( $plugin ) {
404
405 $plugins = get_site_option( 'active_sitewide_plugins' );
406
407 if ( is_multisite() && isset( $plugins[ $plugin ] ) ) {
408 return true;
409 }
410
411 return false;
412 }
413
414
415 /**
416 * Performant utility function for building a term tree.
417 *
418 * Tree will look like this:
419 * [
420 * WP_Term(
421 * name
422 * slug
423 * children ->[
424 * WP_Term()
425 * ]
426 * ),
427 * WP_Term()
428 * ]
429 *
430 * @param array $all_terms Pass get_terms() as this argument where terms are objects NOT arrays.
431 * @param string|bool $orderby Can be count|name|false. This is how each tree branch will be ordered.
432 * @param string $order Can be asc|desc. This is the direction ordering will occur.
433 * @param bool $flat If false, a tree will be returned e.g. an array of top level terms
434 * which children linked within each node. If true, the tree will be
435 * "flattened".
436 * @since 2.5
437 * @return array
438 */
439 function get_term_tree( $all_terms, $orderby = 'count', $order = 'desc', $flat = false ) {
440 $terms_map = [];
441 $terms_tree = [];
442 $iteration_id = 0;
443
444 while ( true ) {
445 if ( empty( $all_terms ) ) {
446 break;
447 }
448
449 foreach ( $all_terms as $key => $term ) {
450 ++$iteration_id;
451
452 if ( ! isset( $term->children ) ) {
453 $term->children = [];
454 }
455
456 if ( ! isset( $terms_map[ $term->term_id ] ) ) {
457 $terms_map[ $term->term_id ] = $term;
458 }
459
460 $parent_term = get_term( $term->parent, $term->taxonomy );
461
462 if ( empty( $term->parent ) || is_wp_error( $parent_term ) || ! $parent_term ) {
463 $term->level = 0;
464
465 if ( empty( $orderby ) ) {
466 $terms_tree[] = $term;
467 } elseif ( 'count' === $orderby ) {
468 /**
469 * We add this weird number to get past terms with the same count
470 */
471 $terms_tree[ ( ( $term->count * 10000000 ) + $iteration_id ) ] = $term;
472 } elseif ( 'name' === $orderby ) {
473 $terms_tree[ strtolower( $term->name ) ] = $term;
474 }
475
476 unset( $all_terms[ $key ] );
477 } elseif ( ! empty( $terms_map[ $term->parent ] ) && isset( $terms_map[ $term->parent ]->level ) ) {
478
479 if ( empty( $orderby ) ) {
480 $terms_map[ $term->parent ]->children[] = $term;
481 } elseif ( 'count' === $orderby ) {
482 $terms_map[ $term->parent ]->children[ ( ( $term->count * 10000000 ) + $iteration_id ) ] = $term;
483 } elseif ( 'name' === $orderby ) {
484 $terms_map[ $term->parent ]->children[ $term->name ] = $term;
485 }
486
487 $parent_level = ( $terms_map[ $term->parent ]->level ) ? $terms_map[ $term->parent ]->level : 0;
488
489 $term->level = $parent_level + 1;
490 $term->parent_term = $terms_map[ $term->parent ];
491
492 unset( $all_terms[ $key ] );
493 }
494 }
495 }
496
497 if ( ! empty( $orderby ) ) {
498 if ( 'asc' === $order ) {
499 ksort( $terms_tree );
500 } else {
501 krsort( $terms_tree );
502 }
503
504 foreach ( $terms_map as $term ) {
505 if ( 'asc' === $order ) {
506 ksort( $term->children );
507 } else {
508 krsort( $term->children );
509 }
510
511 $term->children = array_values( $term->children );
512 }
513
514 $terms_tree = array_values( $terms_tree );
515 }
516
517 if ( $flat ) {
518 $flat_tree = [];
519
520 foreach ( $terms_tree as $term ) {
521 $flat_tree[] = $term;
522 $to_process = $term->children;
523 while ( ! empty( $to_process ) ) {
524 $term = array_shift( $to_process );
525 $flat_tree[] = $term;
526
527 if ( ! empty( $term->children ) ) {
528 $to_process = array_merge( $term->children, $to_process );
529 }
530 }
531 }
532
533 return $flat_tree;
534 }
535
536 return $terms_tree;
537 }
538
539 /**
540 * Returns the default language for ES mapping.
541 *
542 * @return string Default EP language.
543 */
544 function get_language() {
545 $ep_language = get_option( 'ep_language' );
546 $ep_language = ! empty( $ep_language ) ? $ep_language : 'site-default';
547
548 /**
549 * Filter the default language to use at index time
550 *
551 * @since 3.1
552 * @param {string} The current language.
553 * @hook ep_default_language
554 * @return {string} New language
555 */
556 return apply_filters( 'ep_default_language', $ep_language );
557 }
558
559 /**
560 * Returns the status of an ongoing index operation.
561 *
562 * Returns the status of an ongoing index operation in array with the following fields:
563 * indexing | boolean | True if index operation is ongoing or false
564 * method | string | 'cli', 'web' or 'none'
565 * items_indexed | integer | Total number of items indexed
566 * total_items | integer | Total number of items indexed or -1 if not yet determined
567 * slug | string | The slug of the indexable
568 *
569 * @since 3.5.2
570 * @return array|boolean
571 */
572 function get_indexing_status() {
573
574 $index_status = false;
575
576 $index_meta = IndexHelper::factory()->get_index_meta();
577
578 if ( ! empty( $index_meta ) ) {
579 $index_status = $index_meta;
580
581 $index_status['indexing'] = true;
582
583 if ( ! empty( $index_meta['current_sync_item'] ) ) {
584 $index_status['items_indexed'] = $index_meta['current_sync_item']['synced'];
585 $index_status['url'] = $index_meta['current_sync_item']['url'] ?? ''; // Global indexables won't have a url.
586 $index_status['total_items'] = $index_meta['current_sync_item']['total'];
587 $index_status['slug'] = $index_meta['current_sync_item']['indexable'];
588 }
589
590 // Change method name for retrocompatibility.
591 // `dashboard` is used mainly because hooks names depend on that.
592 if ( ! empty( $index_status['method'] ) && 'dashboard' === $index_status['method'] ) {
593 $index_status['method'] = 'web';
594 }
595
596 if ( ! empty( $index_status['method'] ) && 'web' === $index_status['method'] ) {
597 $should_interrupt_sync = filter_var(
598 get_transient( 'ep_sync_interrupted' ),
599 FILTER_VALIDATE_BOOLEAN
600 );
601
602 $index_status['should_interrupt_sync'] = $should_interrupt_sync;
603 }
604 }
605
606 return $index_status;
607 }
608
609 /**
610 * Use the correct update option function depending on the context (multisite or not)
611 *
612 * @since 3.6.0
613 * @param string $option Name of the option to update.
614 * @param mixed $value Option value.
615 * @param mixed $autoload Whether to load the option when WordPress starts up.
616 * @return bool
617 */
618 function update_option( $option, $value, $autoload = null ) {
619 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
620 return \update_site_option( $option, $value );
621 }
622 return \update_option( $option, $value, $autoload );
623 }
624
625 /**
626 * Use the correct get option function depending on the context (multisite or not)
627 *
628 * @since 3.6.0
629 * @param string $option Name of the option to get.
630 * @param mixed $default_value Default value.
631 * @return mixed
632 */
633 function get_option( $option, $default_value = false ) {
634 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
635 return \get_site_option( $option, $default_value );
636 }
637 return \get_option( $option, $default_value );
638 }
639
640 /**
641 * Use the correct delete option function depending on the context (multisite or not)
642 *
643 * @since 3.6.0
644 * @param string $option Name of the option to delete.
645 * @return bool
646 */
647 function delete_option( $option ) {
648 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
649 return \delete_site_option( $option );
650 }
651 return \delete_option( $option );
652 }
653
654 /**
655 * Check if queries for the current request are going to be integrated with
656 * ElasticPress.
657 *
658 * Public requests and REST API requests are integrated by default, but admin
659 * requests will only be integrated in if the `ep_admin_wp_query_integration`
660 * filter returns `true`, and and admin-ajax.php requests will only be
661 * integrated if the `ep_ajax_wp_query_integration` filter returns `true`.
662 *
663 * If specific types of requests are passed, true will only be returned if the
664 * current request also matches one of the passed types.
665 *
666 * This function is used by features to determine whether they should hook into
667 * the current request.
668 *
669 * @param string $context Slug of the feature that is performing the check.
670 * Passed to the `ep_is_integrated_request` filter.
671 * @param string[] $types Which types of request to check. Any of 'admin',
672 * 'ajax', 'public', and 'rest'. Defaults to all
673 * types.
674 * @return bool Whether the current request supports ElasticPress integration
675 * and is of a given type.
676 *
677 * @since 3.6.0
678 */
679 function is_integrated_request( $context, $types = [] ) {
680 if ( empty( $types ) ) {
681 $types = [ 'admin', 'ajax', 'public', 'rest' ];
682 }
683
684 $is_admin_request = is_admin();
685 $is_ajax_request = wp_doing_ajax();
686 $is_rest_request = defined( 'REST_REQUEST' ) && REST_REQUEST;
687 $is_integrated_admin_request = false;
688 $is_integrated_ajax_request = false;
689 $is_integrated_public_request = false;
690 $is_integrated_rest_request = false;
691
692 if ( $is_admin_request && ! $is_ajax_request && in_array( 'admin', $types, true ) ) {
693
694 /**
695 * Filter whether to integrate with admin queries.
696 *
697 * @hook ep_admin_wp_query_integration
698 * @param bool $integrate True to integrate.
699 * @return bool New value.
700 */
701 $is_integrated_admin_request = apply_filters( 'ep_admin_wp_query_integration', false );
702 }
703
704 if ( $is_ajax_request && in_array( 'ajax', $types, true ) ) {
705
706 /**
707 * Filter to integrate with admin ajax queries.
708 *
709 * @hook ep_ajax_wp_query_integration
710 * @param bool $integrate True to integrate.
711 * @return bool New value.
712 */
713 $is_integrated_ajax_request = apply_filters( 'ep_ajax_wp_query_integration', false );
714 }
715
716 if ( $is_rest_request && in_array( 'rest', $types, true ) ) {
717 $is_integrated_rest_request = true;
718 }
719
720 if ( ! $is_admin_request && ! $is_ajax_request && ! $is_rest_request && in_array( 'public', $types, true ) ) {
721 $is_integrated_public_request = true;
722 }
723
724 /**
725 * Is the current request any of the supported requests.
726 */
727 $is_integrated = (
728 $is_integrated_admin_request ||
729 $is_integrated_ajax_request ||
730 $is_integrated_public_request ||
731 $is_integrated_rest_request
732 );
733
734 /**
735 * Filter whether the queries for the current request should be integrated.
736 *
737 * @hook ep_is_integrated_request
738 * @param bool $is_integrated Whether queries for the request will be
739 * integrated.
740 * @param string $context Context for the original check. Usually the
741 * slug of the feature doing the check.
742 * @param array $types Which requests types are being checked.
743 * @return bool Whether queries for the request will be integrated.
744 *
745 * @since 3.6.2
746 */
747 return apply_filters( 'ep_is_integrated_request', $is_integrated, $context, $types );
748 }
749
750 /**
751 * Get asset info from extracted asset files
752 *
753 * @param string $slug Asset slug as defined in build/webpack configuration
754 * @param string $attribute Optional attribute to get. Can be version or dependencies
755 * @return string|array
756 */
757 function get_asset_info( $slug, $attribute = null ) {
758 if ( file_exists( EP_PATH . 'dist/js/' . $slug . '.asset.php' ) ) {
759 $asset = require EP_PATH . 'dist/js/' . $slug . '.asset.php';
760 } elseif ( file_exists( EP_PATH . 'dist/css/' . $slug . '.asset.php' ) ) {
761 $asset = require EP_PATH . 'dist/css/' . $slug . '.asset.php';
762 } else {
763 return null;
764 }
765
766 if ( ! empty( $attribute ) && isset( $asset[ $attribute ] ) ) {
767 return $asset[ $attribute ];
768 }
769
770 return $asset;
771 }
772
773 /**
774 * Return the Sync Page URL.
775 *
776 * @since 4.4.0
777 * @param boolean|string $do_sync Whether the link should or should not start a resync. Pass a string to store the reason of the resync.
778 * @return string
779 */
780 function get_sync_url( $do_sync = false ): string {
781 $page = 'admin.php?page=elasticpress-sync';
782 if ( $do_sync ) {
783 $page .= '&do_sync';
784 if ( is_string( $do_sync ) ) {
785 $page .= '=' . rawurlencode( $do_sync );
786 }
787 $page .= '&ep_sync_nonce=' . wp_create_nonce( 'ep_sync_nonce' );
788 }
789 return ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ?
790 network_admin_url( $page ) :
791 admin_url( $page );
792 }
793
794 /**
795 * Check if the `do_sync` parameter is set and the nonce is valid.
796 *
797 * @since 5.1.2
798 * @return boolean
799 */
800 function isset_do_sync_parameter(): bool {
801 return isset( $_GET['do_sync'] ) && ! empty( $_GET['ep_sync_nonce'] ) && wp_verify_nonce( sanitize_key( $_GET['ep_sync_nonce'] ), 'ep_sync_nonce' );
802 }
803
804 /**
805 * Generate a common prefix to be used while generating a request ID.
806 *
807 * Uses the return of `get_index_prefix()` by default.
808 *
809 * @since 4.5.0
810 * @return string
811 */
812 function get_request_id_base() {
813 /**
814 * Filter the base of requests IDs. Uses the return of `get_index_prefix()` by default.
815 *
816 * @hook ep_request_id_base
817 * @since 4.5.0
818 * @param {string} $request_id_base Request ID base
819 * @return {string} New Request ID base
820 */
821 return apply_filters( 'ep_request_id_base', str_replace( '-', '', get_index_prefix() ) );
822 }
823
824 /**
825 * Generate a Request ID.
826 *
827 * The function concatenates the indices prefix to a random UUID4.
828 *
829 * @since 4.5.0
830 * @return string
831 */
832 function generate_request_id(): string {
833 $uuid = str_replace( '-', '', wp_generate_uuid4() );
834
835 /**
836 * Filter the ID generated to identify a request.
837 *
838 * @hook ep_request_id
839 * @since 4.5.0
840 * @param {string} $request_id Request ID. By default formed by the indices prefix and a random UUID4.
841 * @return {string} New Request ID
842 */
843 return apply_filters( 'ep_request_id', get_request_id_base() . $uuid );
844 }
845
846 /**
847 * Given an Elasticsearch response, try to find an error message.
848 *
849 * @since 4.6.0
850 * @param mixed $response The Elasticsearch response
851 * @return string
852 */
853 function get_elasticsearch_error_reason( $response ): string {
854 if ( is_string( $response ) ) {
855 return $response;
856 }
857
858 if ( ! is_array( $response ) ) {
859 return var_export( $response, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
860 }
861
862 if ( ! empty( $response['reason'] ) ) {
863 return (string) $response['reason'];
864 }
865
866 if ( ! empty( $response['result']['error'] ) && ! empty( $response['result']['error']['root_cause'][0]['reason'] ) ) {
867 return (string) $response['result']['error']['root_cause'][0]['reason'];
868 }
869
870 if ( ! empty( $response['result']['errors'] ) && ! empty( $response['result']['items'] ) ) {
871 $error = '';
872 foreach ( $response['result']['items'] as $item ) {
873 if ( ! empty( $item['index']['error']['reason'] ) ) {
874 $error = $item['index']['error']['reason'];
875 break;
876 }
877 }
878 return $error;
879 }
880
881 return '';
882 }
883
884 /**
885 * Use the correct set_transient option function depending on the context (multisite or not)
886 *
887 * @since 4.7.0
888 * @param string $transient Transient name. Expected to not be SQL-escaped.
889 * Must be 172 characters or fewer in length.
890 * @param mixed $value Transient value. Must be serializable if non-scalar.
891 * Expected to not be SQL-escaped.
892 * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
893 * @return bool True if the value was set, false otherwise.
894 */
895 function set_transient( $transient, $value, $expiration = 0 ) {
896 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
897 return \set_site_transient( $transient, $value, $expiration );
898 }
899 return \set_transient( $transient, $value, $expiration );
900 }
901
902 /**
903 * Use the correct get_transient function depending on the context (multisite or not)
904 *
905 * @since 4.7.0
906 * @param string $transient Transient name. Expected to not be SQL-escaped.
907 * @return mixed Value of transient.
908 */
909 function get_transient( $transient ) {
910 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
911 return \get_site_transient( $transient );
912 }
913 return \get_transient( $transient );
914 }
915
916 /**
917 * Use the correct delete_transient function depending on the context (multisite or not)
918 *
919 * @since 4.7.0
920 * @param string $transient Transient name. Expected to not be SQL-escaped.
921 * @return bool True if the transient was deleted, false otherwise.
922 */
923 function delete_transient( $transient ) {
924 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
925 return \delete_site_transient( $transient );
926 }
927 return \delete_transient( $transient );
928 }
929
930 /**
931 * Whether we are in the top level admin context or not.
932 *
933 * In a single site, the top level admin context would be `is_admin()`,
934 * in a multisite, it would be `is_network_admin()`.
935 *
936 * @since 5.0.0
937 * @return boolean
938 */
939 function is_top_level_admin_context() {
940 $is_network = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK;
941 return $is_network ? is_network_admin() : is_admin();
942 }
943
944 /**
945 * Safely resolve the post type(s) for a taxonomy query.
946 *
947 * Guards against null queried objects, missing taxonomy properties,
948 * deregistered taxonomies, and non-post-type object types that would
949 * otherwise cause unexpected behavior when chaining
950 * get_queried_object()->taxonomy through get_taxonomy().
951 *
952 * @since 5.3.3
953 *
954 * @param \WP_Query|null $query Optional. WP_Query instance. Defaults to the global query.
955 * @return array Registered post type names on success, empty array otherwise.
956 */
957 function get_post_types_for_tax_query( ?\WP_Query $query = null ): array {
958 global $wp_query;
959
960 if ( null === $query ) {
961 $query = $wp_query;
962 }
963
964 if ( ! $query instanceof \WP_Query || ! $query->is_tax() ) {
965 return [];
966 }
967
968 $queried_object = $query->get_queried_object();
969 if ( ! $queried_object || ! isset( $queried_object->taxonomy ) ) {
970 return [];
971 }
972
973 $taxonomy_object = get_taxonomy( $queried_object->taxonomy );
974 if ( ! $taxonomy_object || ! is_array( $taxonomy_object->object_type ) ) {
975 return [];
976 }
977
978 return array_values( array_filter( $taxonomy_object->object_type, 'post_type_exists' ) );
979 }
980