Source: includes/utils.php

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