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 / dashboard.php

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

982 lines 27.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Create an ElasticPress dashboard page.
4 *
5 * @package elasticpress
6 * @since 1.9
7 */
8
9 namespace ElasticPress\Dashboard;
10
11 use ElasticPress\AdminNotices;
12 use ElasticPress\Elasticsearch;
13 use ElasticPress\Features;
14 use ElasticPress\Installer;
15 use ElasticPress\Screen;
16 use ElasticPress\Stats;
17 use ElasticPress\Utils;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit; // Exit if accessed directly.
21 }
22
23 /**
24 * Setup actions and filters for all things settings
25 *
26 * @since 2.1
27 */
28 function setup() {
29 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { // Must be network admin in multisite.
30 add_action( 'network_admin_menu', __NAMESPACE__ . '\action_admin_menu' );
31 add_action( 'admin_bar_menu', __NAMESPACE__ . '\action_network_admin_bar_menu', 50 );
32 }
33
34 add_action( 'admin_menu', __NAMESPACE__ . '\action_admin_menu' );
35 add_action( 'wp_ajax_ep_save_feature', __NAMESPACE__ . '\action_wp_ajax_ep_save_feature' );
36 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\action_admin_enqueue_dashboard_scripts' );
37 add_action( 'admin_init', __NAMESPACE__ . '\maybe_clear_es_info_cache' );
38 add_action( 'admin_init', __NAMESPACE__ . '\maybe_skip_install' );
39 add_action( 'wp_ajax_ep_notice_dismiss', __NAMESPACE__ . '\action_wp_ajax_ep_notice_dismiss' );
40 add_action( 'admin_notices', __NAMESPACE__ . '\maybe_notice' );
41 add_action( 'network_admin_notices', __NAMESPACE__ . '\maybe_notice' );
42 add_filter( 'plugin_action_links', __NAMESPACE__ . '\filter_plugin_action_links', 10, 2 );
43 add_filter( 'network_admin_plugin_action_links', __NAMESPACE__ . '\filter_plugin_action_links', 10, 2 );
44 add_action( 'ep_add_query_log', __NAMESPACE__ . '\log_version_query_error' );
45 add_filter( 'ep_analyzer_language', __NAMESPACE__ . '\use_language_in_setting', 10, 2 );
46 add_filter( 'wp_kses_allowed_html', __NAMESPACE__ . '\filter_allowed_html', 10, 2 );
47 add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\block_assets' );
48 add_action( 'init', __NAMESPACE__ . '\register_react_jsx_runtime', 1 );
49
50 if ( version_compare( get_bloginfo( 'version' ), '5.8', '>=' ) ) {
51 add_action( 'block_categories_all', __NAMESPACE__ . '\block_categories' );
52 } else {
53 add_action( 'block_categories', __NAMESPACE__ . '\block_categories' );
54 }
55
56 /**
57 * Filter whether to show 'ElasticPress Indexing' option on Multisite in admin UI or not.
58 *
59 * @since 3.6.0
60 * @hook ep_show_indexing_option_on_multisite
61 * @param {bool} $show True to show.
62 * @return {bool} New value
63 */
64 $show_indexing_option_on_multisite = apply_filters( 'ep_show_indexing_option_on_multisite', defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK );
65
66 if ( $show_indexing_option_on_multisite ) {
67 add_filter( 'wpmu_blogs_columns', __NAMESPACE__ . '\filter_blogs_columns', 10, 1 );
68 add_action( 'manage_sites_custom_column', __NAMESPACE__ . '\add_blogs_column', 10, 2 );
69 add_action( 'wp_ajax_ep_site_admin', __NAMESPACE__ . '\action_wp_ajax_ep_site_admin' );
70 }
71 }
72
73 /**
74 * Register the `react-jsx-runtime` script handle on WordPress versions that
75 * do not ship it. WordPress added it in 6.6; 10up-toolkit emits it as a
76 * dependency of the React admin and Instant Results bundles, and without the
77 * handle those scripts never print on WordPress 6.2.
78 *
79 * @since 5.3.4
80 * @return void
81 */
82 function register_react_jsx_runtime() {
83 if ( wp_script_is( 'react-jsx-runtime', 'registered' ) ) {
84 return;
85 }
86
87 wp_register_script( 'react-jsx-runtime', false, [ 'react' ], '18', true );
88 wp_add_inline_script(
89 'react-jsx-runtime',
90 'window.ReactJSXRuntime=window.ReactJSXRuntime||{Fragment:window.React.Fragment,jsx:function(type,config,maybeKey){var props=Object.assign({},config);if(maybeKey!==undefined){props.key=maybeKey;}return window.React.createElement(type,props);},jsxs:function(type,config,maybeKey){var props=Object.assign({},config);if(maybeKey!==undefined){props.key=maybeKey;}return window.React.createElement(type,props);}};'
91 );
92 }
93
94 /**
95 * Add ep-html kses context
96 *
97 * @param array $allowedtags HTML tags
98 * @param string $context Context string
99 * @since 3.0
100 * @return array
101 */
102 function filter_allowed_html( $allowedtags, $context ) {
103 global $allowedposttags;
104
105 if ( 'ep-html' === $context ) {
106 $ep_tags = $allowedposttags;
107
108 $atts = [
109 'type' => true,
110 'checked' => true,
111 'selected' => true,
112 'disabled' => true,
113 'value' => true,
114 'href' => true,
115 'class' => true,
116 'data-*' => true,
117 'data-field-name' => true,
118 'data-ep-notice' => true,
119 'data-feature' => true,
120 'id' => true,
121 'style' => true,
122 'title' => true,
123 'name' => true,
124 'placeholder' => '',
125 ];
126
127 $ep_tags['input'] = $atts;
128 $ep_tags['select'] = $atts;
129 $ep_tags['textarea'] = $atts;
130 $ep_tags['option'] = $atts;
131
132 $ep_tags['form'] = [
133 'action' => true,
134 'accept' => true,
135 'accept-charset' => true,
136 'enctype' => true,
137 'method' => true,
138 'name' => true,
139 'target' => true,
140 ];
141
142 $ep_tags['a'] = array_merge(
143 $atts,
144 [ 'target' => true ]
145 );
146
147 return $ep_tags;
148 }
149
150 return $allowedtags;
151 }
152
153 /**
154 * Stores the results of the version query.
155 *
156 * @param array $query The version query.
157 * @since 3.0
158 */
159 function log_version_query_error( $query ) {
160 // Ignore fake requests like the autosuggest template generation
161 if ( ! empty( $query['request'] ) && is_array( $query['request'] ) && ! empty( $query['request']['is_ep_fake_request'] ) ) {
162 return;
163 }
164
165 $logging_key = 'logging_ep_es_info';
166
167 $logging = Utils\get_transient( $logging_key );
168
169 // Are we logging the version query results?
170 if ( '1' === $logging ) {
171 /**
172 * Filter how long results of Elasticsearch version query are stored
173 *
174 * @since 23.0
175 * @hook ep_es_info_cache_expiration
176 * @param {int} Time in seconds
177 * @return {int} New time in seconds
178 */
179 $cache_time = apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) );
180 $response_code_key = 'ep_es_info_response_code';
181 $response_error_key = 'ep_es_info_response_error';
182 $response_code = 0;
183 $response_error = '';
184
185 if ( ! empty( $query['request'] ) ) {
186 $response_code = absint( wp_remote_retrieve_response_code( $query['request'] ) );
187 $response_error = wp_remote_retrieve_response_message( $query['request'] );
188 if ( empty( $response_error ) && is_wp_error( $query['request'] ) ) {
189 $response_error = $query['request']->get_error_message();
190 }
191 }
192
193 // Store the response code, and remove the flag that says
194 // we're logging the response code so we don't log additional
195 // queries.
196 Utils\set_transient( $response_code_key, $response_code, $cache_time );
197 Utils\set_transient( $response_error_key, $response_error, $cache_time );
198 Utils\delete_transient( $logging_key );
199 }
200 }
201
202 /**
203 * Allow user to skip install process.
204 *
205 * @since 3.5
206 */
207 function maybe_skip_install() {
208 if ( ! is_admin() && ! is_network_admin() ) {
209 return;
210 }
211
212 if ( empty( $_GET['ep-skip-install'] ) || empty( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['nonce'] ), 'ep-skip-install' ) || ! in_array( Screen::factory()->get_current_screen(), [ 'install' ], true ) ) {
213 return;
214 }
215
216 if ( ! empty( $_GET['ep-skip-features'] ) ) {
217 $features = \ElasticPress\Features::factory()->registered_features;
218
219 foreach ( $features as $slug => $feature ) {
220 \ElasticPress\Features::factory()->deactivate_feature( $slug );
221 }
222 }
223
224 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
225 $redirect_url = network_admin_url( 'admin.php?page=elasticpress' );
226 } else {
227 $redirect_url = admin_url( 'admin.php?page=elasticpress' );
228 }
229 Utils\update_option( 'ep_skip_install', true );
230
231 wp_safe_redirect( $redirect_url );
232 exit;
233 }
234
235 /**
236 * Clear ES info cache whenever EP dash or settings page is viewed. Also clear cache
237 * when "try again" notification link is clicked.
238 *
239 * @since 2.3.1
240 */
241 function maybe_clear_es_info_cache() {
242 if ( ! is_admin() && ! is_network_admin() ) {
243 return;
244 }
245
246 $isset_retry = ! empty( $_GET['ep-retry'] ) &&
247 ! empty( $_GET['ep_retry_nonce'] ) &&
248 wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['ep_retry_nonce'] ) ), 'ep_retry_nonce' );
249
250 if ( ! $isset_retry && ! in_array( Screen::factory()->get_current_screen(), [ 'dashboard', 'settings', 'install' ], true ) ) {
251 return;
252 }
253
254 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
255 delete_site_transient( 'ep_es_info' );
256 } else {
257 delete_transient( 'ep_es_info' );
258 }
259
260 if ( $isset_retry ) {
261 wp_safe_redirect( remove_query_arg( [ 'ep-retry', 'ep_retry_nonce' ] ) );
262 exit();
263 }
264 }
265
266 /**
267 * Show ElasticPress in network admin menu bar
268 *
269 * @param object $admin_bar WP_Admin Bar reference.
270 * @since 2.2
271 */
272 function action_network_admin_bar_menu( $admin_bar ) {
273 $admin_bar->add_menu(
274 array(
275 'id' => 'network-admin-elasticpress',
276 'parent' => 'network-admin',
277 'title' => 'ElasticPress',
278 'href' => esc_url( network_admin_url( 'admin.php?page=elasticpress' ) ),
279 )
280 );
281 }
282
283 /**
284 * Output dashboard link in plugin actions
285 *
286 * @param array $plugin_actions Array of HTML.
287 * @param string $plugin_file Path to plugin file.
288 * @since 2.1
289 * @return array
290 */
291 function filter_plugin_action_links( $plugin_actions, $plugin_file ) {
292
293 if ( is_network_admin() ) {
294 $url = admin_url( 'network/admin.php?page=elasticpress' );
295
296 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
297 return $plugin_actions;
298 }
299 } else {
300 $url = admin_url( 'admin.php?page=elasticpress' );
301
302 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
303 return $plugin_actions;
304 }
305 }
306
307 $new_actions = [];
308
309 if ( basename( EP_PATH ) . '/elasticpress.php' === $plugin_file ) {
310 $new_actions['ep_dashboard'] = sprintf( '<a href="%s">%s</a>', esc_url( $url ), __( 'Dashboard', 'elasticpress' ) );
311 }
312
313 return array_merge( $new_actions, $plugin_actions );
314 }
315
316 /**
317 * Output variety of dashboard notices.
318 *
319 * @param bool $force Force ES info hard lookup.
320 * @since 3.0
321 */
322 function maybe_notice( $force = false ) {
323 if ( ! current_user_can( Utils\get_capability() ) ) {
324 return false;
325 }
326
327 /**
328 * Filter how long results of Elasticsearch version query are stored
329 *
330 * @since 23.0
331 * @hook ep_es_info_cache_expiration
332 * @param {int} Time in seconds
333 * @return {int} New time in seconds
334 */
335 $cache_time = apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) );
336
337 Utils\set_transient(
338 'logging_ep_es_info',
339 '1',
340 $cache_time
341 );
342
343 // Fetch ES version
344 Elasticsearch::factory()->get_elasticsearch_version( $force );
345
346 AdminNotices::factory()->process_notices();
347
348 $notices = AdminNotices::factory()->get_notices();
349
350 foreach ( $notices as $notice_key => $notice ) {
351 $class = 'notice notice-' . $notice['type'];
352 if ( $notice['dismiss'] ) {
353 $class .= ' is-dismissible';
354 }
355 ?>
356 <div data-ep-notice="<?php echo esc_attr( $notice_key ); ?>" class="<?php echo esc_attr( $class ); ?>">
357 <p>
358 <?php echo wp_kses( $notice['html'], 'ep-html' ); ?>
359 </p>
360 </div>
361 <?php
362 }
363
364 wp_enqueue_script( 'ep_notice_script' );
365
366 return $notices;
367 }
368
369 /**
370 * Dismiss notice via ajax
371 *
372 * @since 2.2
373 */
374 function action_wp_ajax_ep_notice_dismiss() {
375 if ( empty( $_POST['notice'] ) || ! check_ajax_referer( 'ep_admin_nonce', 'nonce', false ) ) {
376 wp_send_json_error();
377 exit;
378 }
379
380 if ( ! current_user_can( Utils\get_capability() ) ) {
381 wp_send_json_error();
382 exit;
383 }
384
385 AdminNotices::factory()->dismiss_notice( sanitize_key( $_POST['notice'] ) );
386
387 wp_send_json_success();
388 }
389
390 /**
391 * Getting the status of ongoing index fired by WP CLI
392 *
393 * @since 2.1
394 */
395 function action_wp_ajax_ep_cli_index() {
396 _deprecated_function( __CLASS__, '3.6.0', '\ElasticPress\Screen::factory()->sync_screen->action_wp_ajax_ep_cli_index()' );
397 }
398
399 /**
400 * Continue index
401 *
402 * @since 2.1
403 */
404 function action_wp_ajax_ep_index() {
405 _deprecated_function( __CLASS__, '3.6.0', '\ElasticPress\Screen::factory()->sync_screen->action_wp_ajax_ep_index()' );
406 }
407
408 /**
409 * Cancel index
410 *
411 * @since 2.1
412 */
413 function action_wp_ajax_ep_cancel_index() {
414 _deprecated_function( __CLASS__, '3.6.0', '\ElasticPress\Screen::factory()->sync_screen->action_wp_ajax_ep_cancel_index()' );
415 }
416
417 /**
418 * Save individual feature settings
419 *
420 * @since 2.2
421 */
422 function action_wp_ajax_ep_save_feature() {
423 $post = wp_unslash( $_POST );
424
425 if ( empty( $post['feature'] ) || empty( $post['settings'] ) || ! check_ajax_referer( 'ep_dashboard_nonce', 'nonce', false ) ) {
426 wp_send_json_error();
427 exit;
428 }
429
430 if ( Utils\is_indexing() ) {
431 $error = new \WP_Error( 'is_indexing' );
432
433 wp_send_json_error( $error );
434 exit;
435 }
436
437 $data = Features::factory()->update_feature( $post['feature'], $post['settings'] );
438
439 // Since we deactivated, delete auto activate notice.
440 if ( empty( $post['settings']['active'] ) ) {
441 Utils\delete_option( 'ep_feature_auto_activated_sync' );
442 }
443
444 wp_send_json_success( $data );
445 }
446
447 /**
448 * Register and Enqueue JavaScripts for dashboard
449 *
450 * @since 2.2
451 */
452 function action_admin_enqueue_dashboard_scripts() {
453 if ( isset( get_current_screen()->id ) && strpos( get_current_screen()->id, 'sites-network' ) !== false ) {
454 wp_enqueue_style( 'wp-components' );
455
456 wp_enqueue_script(
457 'ep_admin_sites_scripts',
458 EP_URL . 'dist/js/sites-admin-script.js',
459 Utils\get_asset_info( 'sites-admin-script', 'dependencies' ),
460 Utils\get_asset_info( 'sites-admin-script', 'version' ),
461 true
462 );
463
464 wp_set_script_translations( 'ep_admin_sites_scripts', 'elasticpress' );
465
466 $data = [
467 'ajax_url' => admin_url( 'admin-ajax.php' ),
468 'nonce' => wp_create_nonce( 'epsa' ),
469 ];
470
471 wp_localize_script( 'ep_admin_sites_scripts', 'epsa', $data );
472 }
473
474 $general_ep_screens = [ 'dashboard', 'settings', 'install', 'health', 'weighting', 'synonyms', 'sync', 'status-report' ];
475 /**
476 * Filter the query logger object
477 *
478 * @since 5.3.3
479 * @hook elasticpress_general_ep_screens
480 * @param {array} $general_ep_screens The general ElasticPress screens.
481 * @return {array} The filtered general ElasticPress screens.
482 */
483 $general_ep_screens = apply_filters( 'elasticpress_general_ep_screens', $general_ep_screens );
484 if ( in_array( Screen::factory()->get_current_screen(), $general_ep_screens, true ) ) {
485 wp_enqueue_style(
486 'ep_admin_styles',
487 EP_URL . 'dist/css/dashboard-styles.css',
488 Utils\get_asset_info( 'dashboard-styles', 'dependencies' ),
489 Utils\get_asset_info( 'dashboard-styles', 'version' )
490 );
491 wp_enqueue_script(
492 'ep_admin_script',
493 EP_URL . 'dist/js/admin-script.js',
494 Utils\get_asset_info( 'admin-script', 'dependencies' ),
495 Utils\get_asset_info( 'admin-script', 'version' ),
496 true
497 );
498
499 wp_set_script_translations( 'ep_admin_script', 'elasticpress' );
500 }
501
502 if ( in_array( Screen::factory()->get_current_screen(), [ 'dashboard', 'install' ], true ) ) {
503 wp_enqueue_script(
504 'ep_dashboard_scripts',
505 EP_URL . 'dist/js/dashboard-script.js',
506 Utils\get_asset_info( 'dashboard-script', 'dependencies' ),
507 Utils\get_asset_info( 'dashboard-script', 'version' ),
508 true
509 );
510
511 wp_set_script_translations( 'ep_dashboard_scripts', 'elasticpress' );
512
513 $sync_url = Utils\get_sync_url( true );
514
515 $skip_url = ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ?
516 network_admin_url( 'admin.php?page=elasticpress' ) :
517 admin_url( 'admin.php?page=elasticpress' );
518
519 $data = array(
520 'skipUrl' => add_query_arg(
521 array(
522 'ep-skip-install' => 1,
523 'ep-skip-features' => 1,
524 'nonce' => wp_create_nonce( 'ep-skip-install' ),
525 ),
526 $skip_url
527 ),
528 'syncUrl' => $sync_url,
529 );
530
531 wp_localize_script( 'ep_dashboard_scripts', 'epDash', $data );
532 }
533
534 if ( in_array( Screen::factory()->get_current_screen(), [ 'health' ], true ) && ! empty( Utils\get_host() ) ) {
535 Stats::factory()->build_stats();
536
537 $data = Stats::factory()->get_localized();
538
539 wp_enqueue_script(
540 'ep_stats',
541 EP_URL . 'dist/js/stats-script.js',
542 Utils\get_asset_info( 'stats-script', 'dependencies' ),
543 Utils\get_asset_info( 'stats-script', 'version' ),
544 true
545 );
546
547 wp_set_script_translations( 'ep_stats', 'elasticpress' );
548
549 wp_localize_script( 'ep_stats', 'epChartData', $data );
550 }
551
552 wp_register_script(
553 'ep_notice_script',
554 EP_URL . 'dist/js/notice-script.js',
555 Utils\get_asset_info( 'notice-script', 'dependencies' ),
556 Utils\get_asset_info( 'notice-script', 'version' ),
557 true
558 );
559
560 wp_set_script_translations( 'ep_notice_script', 'elasticpress' );
561
562 wp_localize_script(
563 'ep_notice_script',
564 'epAdmin',
565 array(
566 'nonce' => wp_create_nonce( 'ep_admin_nonce' ),
567 )
568 );
569
570 wp_enqueue_style(
571 'ep_general_styles',
572 EP_URL . 'dist/css/general-styles.css',
573 Utils\get_asset_info( 'general-styles', 'dependencies' ),
574 Utils\get_asset_info( 'general-styles', 'version' )
575 );
576 }
577
578 /**
579 * Output current ElasticPress dashboard screen
580 *
581 * @since 3.0
582 */
583 function resolve_screen() {
584 Screen::factory()->output();
585 }
586
587 /**
588 * Admin menu actions
589 *
590 * Adds options page to admin menu.
591 *
592 * @since 1.9
593 * @return void
594 */
595 function action_admin_menu() {
596 Installer::factory()->calculate_install_status();
597 if ( true !== Installer::factory()->get_install_status() && ! Utils\is_top_level_admin_context() ) {
598 return;
599 }
600
601 if ( ! Utils\is_site_indexable() && ! is_network_admin() ) {
602 return;
603 }
604
605 $capability = ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ? Utils\get_network_capability() : Utils\get_capability();
606
607 add_menu_page(
608 'ElasticPress',
609 'ElasticPress',
610 $capability,
611 'elasticpress',
612 __NAMESPACE__ . '\resolve_screen',
613 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNzMgNzEuMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNzMgNzEuMzsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0zNi41LDQuN0MxOS40LDQuNyw1LjYsMTguNiw1LjYsMzUuN2MwLDEwLDQuNywxOC45LDEyLjEsMjQuNWw0LjUtNC41YzAuMS0wLjEsMC4xLTAuMiwwLjItMC4zbDAuNy0wLjdsNi40LTYuNGMyLjEsMS4yLDQuNSwxLjksNy4xLDEuOWM4LDAsMTQuNS02LjUsMTQuNS0xNC41cy02LjUtMTQuNS0xNC41LTE0LjVTMjIsMjcuNiwyMiwzNS42YzAsMi44LDAuOCw1LjMsMi4xLDcuNWwtNi40LDYuNGMtMi45LTMuOS00LjYtOC43LTQuNi0xMy45YzAtMTIuOSwxMC41LTIzLjQsMjMuNC0yMy40czIzLjQsMTAuNSwyMy40LDIzLjRTNDkuNCw1OSwzNi41LDU5Yy0yLjEsMC00LjEtMC4zLTYtMC44bC0wLjYsMC42bC01LjIsNS40YzMuNiwxLjUsNy42LDIuMywxMS44LDIuM2MxNy4xLDAsMzAuOS0xMy45LDMwLjktMzAuOVM1My42LDQuNywzNi41LDQuN3oiIGZpbGw9IiNhN2FhYWQiLz48L3N2Zz4='
614 );
615
616 if ( ! Utils\is_top_level_admin_context() ) {
617 return;
618 }
619
620 add_submenu_page(
621 'elasticpress',
622 esc_html__( 'ElasticPress Features', 'elasticpress' ),
623 esc_html__( 'Features', 'elasticpress' ),
624 $capability,
625 'elasticpress',
626 __NAMESPACE__ . '\resolve_screen'
627 );
628
629 add_submenu_page(
630 'elasticpress',
631 esc_html__( 'ElasticPress Settings', 'elasticpress' ),
632 esc_html__( 'Settings', 'elasticpress' ),
633 $capability,
634 'elasticpress-settings',
635 __NAMESPACE__ . '\resolve_screen'
636 );
637
638 add_submenu_page(
639 'elasticpress',
640 'ElasticPress ' . esc_html__( 'Sync', 'elasticpress' ),
641 esc_html__( 'Sync', 'elasticpress' ),
642 $capability,
643 'elasticpress-sync',
644 __NAMESPACE__ . '\resolve_screen'
645 );
646
647 add_submenu_page(
648 'elasticpress',
649 esc_html__( 'ElasticPress Index Health', 'elasticpress' ),
650 esc_html__( 'Index Health', 'elasticpress' ),
651 $capability,
652 'elasticpress-health',
653 __NAMESPACE__ . '\resolve_screen'
654 );
655
656 add_submenu_page(
657 'elasticpress',
658 esc_html__( 'ElasticPress Status Report', 'elasticpress' ),
659 esc_html__( 'Status Report', 'elasticpress' ),
660 $capability,
661 'elasticpress-status-report',
662 __NAMESPACE__ . '\resolve_screen'
663 );
664 }
665
666 /**
667 * Languages supported in Elasticsearch mappings.
668 *
669 * If $format is 'elasticsearch', the array format is `Elasticsearch analyzer name => [ WordPress language package names ]`.
670 *
671 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html
672 * @since 4.7.0
673 * @param string $format Format of the return ('locales' or 'elasticsearch' )
674 * @return array
675 */
676 function get_available_languages( string $format = 'elasticsearch' ): array {
677 /**
678 * Filter available languages in Elasticsearch.
679 *
680 * The returned array should follow the format `Elasticsearch analyzer name => [ WordPress language package names ]`.
681 *
682 * @since 4.7.0
683 * @hook ep_available_languages
684 * @param {bool} $available_languages List of available languages
685 * @return {bool} New list
686 */
687 $es_languages = apply_filters(
688 'ep_available_languages',
689 [
690 'arabic' => [ 'ar', 'ary' ],
691 'armenian' => [ 'hy' ],
692 'basque' => [ 'eu' ],
693 'bengali' => [ 'bn', 'bn_BD' ],
694 'brazilian' => [ 'pt_BR' ],
695 'bulgarian' => [ 'bg', 'bg_BG' ],
696 'catalan' => [ 'ca' ],
697 'cjk' => [], // CJK characters (not a language)
698 'czech' => [ 'cs', 'cs_CZ' ],
699 'danish' => [ 'da', 'da_DK' ],
700 'dutch' => [ 'nl_NL_formal', 'nl_NL', 'nl_BE' ],
701 'english' => [ 'en', 'en_AU', 'en_GB', 'en_NZ', 'en_CA', 'en_US', 'en_ZA' ],
702 'estonian' => [ 'et' ],
703 'finnish' => [ 'fi' ],
704 'french' => [ 'fr', 'fr_CA', 'fr_FR', 'fr_BE' ],
705 'galician' => [ 'gl_ES' ],
706 'german' => [ 'de', 'de_DE', 'de_DE_formal', 'de_CH', 'de_CH_informal', 'de_AT' ],
707 'greek' => [ 'el' ],
708 'hindi' => [ 'hi_IN' ],
709 'hungarian' => [ 'hu_HU' ],
710 'indonesian' => [ 'id_ID' ],
711 'irish' => [], // WordPress doesn't support Irish as an active locale currently
712 'italian' => [ 'it_IT' ],
713 'latvian' => [ 'lv' ],
714 'lithuanian' => [ 'lt_LT' ],
715 'norwegian' => [ 'nb_NO' ],
716 'persian' => [ 'fa_IR' ],
717 'portuguese' => [ 'pt', 'pt_AO', 'pt_PT', 'pt_PT_ao90' ],
718 'romanian' => [ 'ro_RO' ],
719 'russian' => [ 'ru_RU' ],
720 'sorani' => [ 'ckb' ],
721 'spanish' => [ 'es_CR', 'es_MX', 'es_VE', 'es_AR', 'es_CL', 'es_GT', 'es_PE', 'es_ES', 'es_UY', 'es_CO' ],
722 'swedish' => [ 'sv_SE' ],
723 'turkish' => [ 'tr_TR' ],
724 'thai' => [ 'th' ],
725 ]
726 );
727
728 if ( 'locales' === $format ) {
729 $arr = array_reduce(
730 $es_languages,
731 function ( $acc, $lang ) {
732 $lang = array_filter(
733 $lang,
734 function ( $locale ) {
735 // English is always added. This removes the duplicates
736 return ! in_array( $locale, [ 'en', 'en_US' ], true );
737 }
738 );
739 $acc = array_merge( $acc, $lang );
740 return $acc;
741 },
742 []
743 );
744 return $arr;
745 }
746
747 return $es_languages;
748 }
749
750 /**
751 * Uses the language from EP settings in mapping.
752 *
753 * @param string $language The current language.
754 * @param string $context The context where the function is running.
755 * @return string The updated language.
756 */
757 function use_language_in_setting( $language = 'english', $context = '' ) {
758 global $locale, $wp_local_package;
759
760 // Get the currently set language.
761 $ep_language = Utils\get_language();
762
763 // Bail early if no EP language is set.
764 if ( empty( $ep_language ) ) {
765 return $language;
766 }
767
768 /**
769 * WordPress does not reset the language when switch_blog() is called.
770 *
771 * @see https://core.trac.wordpress.org/ticket/49263
772 */
773 if ( 'site-default' === $ep_language ) {
774 $locale = null;
775 $wp_local_package = null;
776 $ep_language = get_locale();
777 }
778
779 require_once ABSPATH . 'wp-admin/includes/translation-install.php';
780 $translations = wp_get_available_translations();
781
782 // Default to en_US if not in the array of available translations.
783 if ( ! empty( $translations[ $ep_language ]['english_name'] ) ) {
784 $wp_language = $translations[ $ep_language ]['language'];
785 } else {
786 $wp_language = 'en_US';
787 }
788
789 $es_languages = get_available_languages();
790
791 /**
792 * Languages supported in Elasticsearch snowball token filters.
793 *
794 * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-snowball-tokenfilter.html
795 */
796 $es_snowball_languages = [
797 'Armenian',
798 'Basque',
799 'Catalan',
800 'Danish',
801 'Dutch',
802 'English',
803 'Finnish',
804 'French',
805 'German',
806 'German2', // currently unused
807 'Hungarian',
808 'Italian',
809 'Kp', // currently unused
810 'Lithuanian',
811 'Lovins', // currently unused
812 'Norwegian',
813 'Porter', // currently unused
814 'Portuguese',
815 'Romanian',
816 'Russian',
817 'Spanish',
818 'Swedish',
819 'Turkish',
820 ];
821
822 $es_snowball_similar = [
823 'Brazilian' => 'Portuguese',
824 ];
825
826 foreach ( $es_languages as $analyzer_name => $analyzer_language_codes ) {
827 if ( in_array( $wp_language, $analyzer_language_codes, true ) ) {
828 $language = $analyzer_name;
829 break;
830 }
831 }
832
833 if ( 'filter_ewp_snowball' === $context ) {
834 $uc_first_language = ucfirst( $language );
835 if ( in_array( $uc_first_language, $es_snowball_languages, true ) ) {
836 return $uc_first_language;
837 }
838
839 return $es_snowball_similar[ $uc_first_language ] ?? 'English';
840 }
841
842 if ( 'filter_ep_stop' === $context ) {
843 return "_{$language}_";
844 }
845
846 return $language;
847 }
848
849 /**
850 * Add column to sites admin table.
851 *
852 * @param string[] $columns Array of columns.
853 *
854 * @return string[]
855 */
856 function filter_blogs_columns( $columns ) {
857 $columns['elasticpress'] = esc_html__( 'ElasticPress Indexing', 'elasticpress' );
858
859 return $columns;
860 }
861
862 /**
863 * Populate column with checkbox/switch.
864 *
865 * @param string $column_name The name of the current column.
866 * @param int $blog_id The blog ID.
867 *
868 * @return void | string
869 */
870 function add_blogs_column( $column_name, $blog_id ) {
871 if ( 'elasticpress' !== $column_name ) {
872 return;
873 }
874
875 $site = get_site( $blog_id );
876 if ( $site->deleted || $site->archived || $site->spam ) {
877 return;
878 }
879
880 $is_indexable = get_site_meta( $blog_id, 'ep_indexable', true );
881 $is_indexable = '' !== $is_indexable ? $is_indexable : 'yes';
882
883 printf(
884 '<input %1$s class="index-toggle" data-blog-id="%2$s" disabled type="checkbox">',
885 checked( $is_indexable, 'yes', false ),
886 esc_attr( $blog_id )
887 );
888 }
889
890 /**
891 * AJAX callback to update ep_indexable site option.
892 */
893 function action_wp_ajax_ep_site_admin() {
894 $blog_id = ( ! empty( $_POST['blog_id'] ) ) ? absint( wp_unslash( $_POST['blog_id'] ) ) : - 1;
895 $checked = ( ! empty( $_POST['checked'] ) ) ? sanitize_text_field( wp_unslash( $_POST['checked'] ) ) : 'no';
896
897 if ( - 1 === $blog_id || ! check_ajax_referer( 'epsa', 'nonce', false ) ) {
898 return wp_send_json_error();
899 }
900
901 /**
902 * NOTE: This will be removed in ElasticPress 5.0.0. Implementations should rely on site_meta since 4.7.0.
903 */
904 $result = update_blog_option( $blog_id, 'ep_indexable', $checked );
905
906 $result = update_site_meta( $blog_id, 'ep_indexable', $checked );
907 $data = [
908 'blog_id' => $blog_id,
909 'result' => $result,
910 ];
911
912 return wp_send_json_success( $data );
913 }
914
915 /**
916 * Handle the fetch for indexing status
917 */
918 function handle_indexing_status() {
919 $indexing_status = \ElasticPress\Utils\get_indexing_status();
920
921 $status = array(
922 'method' => '',
923 'items_indexed' => 0,
924 'total_items' => 0,
925 'indexable' => '',
926 );
927
928 if ( ! empty( $indexing_status ) ) {
929 if ( isset( $indexing_status['method'] ) && 'cli' === $indexing_status['method'] ) {
930 $status['method'] = $indexing_status['method'];
931 $status['items_indexed'] = $indexing_status['items_indexed'];
932 $status['total_items'] = $indexing_status['total_items'];
933 $status['indexable'] = $indexing_status['slug'];
934 } else {
935 $status['method'] = 'dashboard';
936 $status['items_indexed'] = isset( $indexing_status['offset'] ) ? $indexing_status['offset'] : 0;
937 $status['total_items'] = isset( $indexing_status['found_items'] ) ? $indexing_status['found_items'] : 0;
938 $status['indexable'] = isset( $indexing_status['current_sync_item']['indexable'] ) ? $indexing_status['current_sync_item']['indexable'] : '';
939 }
940 }
941
942 return $status;
943 }
944
945 /**
946 * Add an ElasticPress block category.
947 *
948 * @param array $block_categories Array of categories for block types.
949 * @return array Array of categories for block types.
950 */
951 function block_categories( $block_categories ) {
952 $block_categories[] = [
953 'slug' => 'elasticpress',
954 'title' => 'ElasticPress',
955 ];
956
957 return $block_categories;
958 }
959
960 /**
961 * Enqueue shared block editor assets.
962 *
963 * @return void
964 */
965 function block_assets() {
966 wp_enqueue_script(
967 'elasticpress-blocks',
968 EP_URL . 'dist/js/blocks-script.js',
969 Utils\get_asset_info( 'blocks-script', 'dependencies' ),
970 Utils\get_asset_info( 'blocks-script', 'version' ),
971 true
972 );
973
974 wp_localize_script(
975 'elasticpress-blocks',
976 'epBlocks',
977 [
978 'syncUrl' => Utils\get_sync_url(),
979 ]
980 );
981 }
982