PluginProbe
ElasticPress / 4.7.2
ElasticPress v4.7.2
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 / dashboard.php

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

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