PluginProbe
ElasticPress / 4.7.0
ElasticPress v4.7.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / dashboard.php

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

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