PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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 / classes / Command.php

Command.php in ElasticPress 5.3.5, at includes/classes/Command.php

1,691 lines 51.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP-CLI command for ElasticPress
4 *
5 * phpcs:disable WordPress.WP.I18n.MissingTranslatorsComment
6 *
7 * @since 3.0
8 * @package elasticpress
9 */
10
11 namespace ElasticPress;
12
13 use WP_CLI_Command;
14 use WP_CLI;
15 use ElasticPress\Command\Utility;
16 use ElasticPress\Elasticsearch;
17 use ElasticPress\FeatureRequirementsStatus;
18 use ElasticPress\Features;
19 use ElasticPress\Indexables;
20 use ElasticPress\Utils;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 // @codeCoverageIgnoreStart
24 exit; // Exit if accessed directly.
25 // @codeCoverageIgnoreEnd
26 }
27
28 /**
29 * CLI Commands for ElasticPress
30 */
31 class Command extends WP_CLI_Command {
32
33 use DeprecatedCommand;
34
35 /**
36 * Holds temporary wp_actions when indexing with pagination
37 *
38 * @since 2.2
39 * @var array
40 */
41 private $temporary_wp_actions = [];
42
43 /**
44 * Holds CLI command position args.
45 *
46 * Useful to share arguments to methods called by hooks.
47 *
48 * @since 4.0.0
49 * @var array
50 */
51 protected $args = [];
52
53 /**
54 * Holds CLI command associative args
55 *
56 * Useful to share arguments to methods called by hooks.
57 *
58 * @since 4.0.0
59 * @var array
60 */
61 protected $assoc_args = [];
62
63 /**
64 * Internal timer.
65 *
66 * @since 4.2.0
67 *
68 * @var float
69 */
70 protected $time_start = null;
71
72 /**
73 * Create Command
74 *
75 * @since 3.5.2
76 */
77 public function __construct() {
78 add_filter( 'pre_transient_ep_wpcli_sync_interrupted', [ Utility::class, 'custom_get_transient' ], 10, 2 );
79 }
80
81 /**
82 * Activate a feature. If a re-indexing is required, you will need to do it manually.
83 *
84 * ## OPTIONS
85 *
86 * <feature-slug>
87 * : The feature slug
88 *
89 * @subcommand activate-feature
90 * @since 2.1
91 * @param array $args Positional CLI args.
92 * @param array $assoc_args Associative CLI args.
93 */
94 public function activate_feature( $args, $assoc_args ) {
95 $this->index_occurring();
96
97 $feature = Features::factory()->get_registered_feature( $args[0] );
98
99 if ( empty( $feature ) ) {
100 WP_CLI::error( esc_html__( 'No feature with that slug is registered', 'elasticpress' ) );
101 }
102
103 if ( $feature->is_active() ) {
104 WP_CLI::error( esc_html__( 'This feature is already active', 'elasticpress' ) );
105 }
106
107 $status = $feature->requirements_status();
108
109 if ( FeatureRequirementsStatus::FORCE_DISABLED === $status->get_code() ) {
110 /* translators: Error message */
111 WP_CLI::error( sprintf( esc_html__( 'Feature requirements are not met: %s', 'elasticpress' ), implode( "\n\n", (array) $status->get_message() ) ) );
112 } elseif ( FeatureRequirementsStatus::MANUALLY_ENABLED === $status->get_code() && ! empty( $status->get_message() ) ) {
113 /* translators: Warning message */
114 WP_CLI::warning( sprintf( esc_html__( 'Feature is usable but there are warnings: %s', 'elasticpress' ), implode( "\n\n", (array) $status->get_message() ) ) );
115 }
116
117 Features::factory()->activate_feature( $feature->slug );
118
119 if ( $feature->requires_install_reindex ) {
120 WP_CLI::warning( esc_html__( 'This feature requires a re-index. You may want to run the index command next.', 'elasticpress' ) );
121 }
122
123 WP_CLI::success( esc_html__( 'Feature activated', 'elasticpress' ) );
124 }
125
126 /**
127 * Deactivate a feature.
128 *
129 * ## OPTIONS
130 *
131 * <feature-slug>
132 * : The feature slug
133 *
134 * @subcommand deactivate-feature
135 * @since 2.1
136 * @param array $args Positional CLI args.
137 * @param array $assoc_args Associative CLI args.
138 */
139 public function deactivate_feature( $args, $assoc_args ) {
140 $this->index_occurring();
141
142 $feature = Features::factory()->get_registered_feature( $args[0] );
143
144 if ( empty( $feature ) ) {
145 WP_CLI::error( esc_html__( 'No feature with that slug is registered', 'elasticpress' ) );
146 }
147
148 $active_features = (array) Features::factory()->get_feature_settings();
149 $active_features_draft = (array) Features::factory()->get_feature_settings_draft();
150
151 $key_current = array_search( $feature->slug, array_keys( $active_features ), true );
152 $key_draft = array_search( $feature->slug, array_keys( $active_features_draft ), true );
153
154 $in_current = false !== $key_current && ! empty( $active_features[ $feature->slug ]['active'] );
155 $in_draft = false !== $key_draft && ! empty( $active_features_draft[ $feature->slug ]['active'] );
156
157 if ( ! $in_current && ! $in_draft ) {
158 WP_CLI::error( esc_html__( 'Feature is not active', 'elasticpress' ) );
159 }
160
161 Features::factory()->deactivate_feature( $feature->slug );
162
163 WP_CLI::success( esc_html__( 'Feature deactivated', 'elasticpress' ) );
164 }
165
166 /**
167 * List features (either active or all).
168 *
169 * ## OPTIONS
170 *
171 * [--all]
172 * : Show all registered features
173 *
174 * @subcommand list-features
175 * @since 2.1
176 * @param array $args Positional CLI args.
177 * @param array $assoc_args Associative CLI args.
178 */
179 public function list_features( $args, $assoc_args ) {
180 $list_all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', null );
181
182 if ( empty( $list_all ) ) {
183 $features = Features::factory()->get_feature_settings();
184
185 WP_CLI::line( esc_html__( 'Active features:', 'elasticpress' ) );
186
187 foreach ( array_keys( $features ) as $feature_slug ) {
188 $feature = Features::factory()->get_registered_feature( $feature_slug );
189
190 if ( $feature->is_active() ) {
191 WP_CLI::line( $feature_slug );
192 }
193 }
194 } else {
195 WP_CLI::line( esc_html__( 'Registered features:', 'elasticpress' ) );
196 $features = wp_list_pluck( Features::factory()->registered_features, 'slug' );
197
198 foreach ( $features as $feature ) {
199 WP_CLI::line( $feature );
200 }
201 }
202 }
203
204 /**
205 * Add document mappings for every indexable.
206 *
207 * Sends plugin put mapping to the current Indexables indices (this will delete the indices.)
208 *
209 * ## OPTIONS
210 *
211 * [--network-wide]
212 * : Force mappings to be sent for every index in the network. `--network-wide` takes an optional argument to limit the number of mappings to be sent where 0 is no limit. For example, `--network-wide=5` would send mappings for only 5 blogs on the network.
213 *
214 * [--indexables=<indexables>]
215 * : List of indexables
216 *
217 * [--ep-host=<host>]
218 * : Custom Elasticsearch host
219 *
220 * [--ep-prefix=<prefix>]
221 * : Custom ElasticPress prefix
222 *
223 * @subcommand put-mapping
224 * @since 0.9
225 * @param array $args Positional CLI args.
226 * @param array $assoc_args Associative CLI args.
227 */
228 public function put_mapping( $args, $assoc_args ) {
229 $this->maybe_change_host( $assoc_args );
230 $this->maybe_change_index_prefix( $assoc_args );
231 $this->connect_check();
232 $this->index_occurring();
233 $this->put_mapping_helper( $args, $assoc_args );
234 }
235
236 /**
237 * Add document mappings for every indexable
238 *
239 * @since 3.0
240 * @param array $args Positional CLI args.
241 * @param array $assoc_args Associative CLI args.
242 * @return boolean
243 */
244 private function put_mapping_helper( $args, $assoc_args ) {
245 $indexables = null;
246
247 if ( ! empty( $assoc_args['indexables'] ) ) {
248 $indexables = explode( ',', str_replace( ' ', '', $assoc_args['indexables'] ) );
249 }
250
251 $non_global_indexable_objects = Indexables::factory()->get_all( false );
252 $global_indexable_objects = Indexables::factory()->get_all( true );
253
254 if ( isset( $assoc_args['network-wide'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
255 if ( ! is_numeric( $assoc_args['network-wide'] ) ) {
256 $assoc_args['network-wide'] = 0;
257 }
258
259 $sites = Utils\get_sites( $assoc_args['network-wide'], true );
260
261 foreach ( $sites as $site ) {
262 switch_to_blog( $site['blog_id'] );
263
264 foreach ( $non_global_indexable_objects as $indexable ) {
265 /**
266 * If user has called out specific indexables to be indexed, only do those
267 */
268 if ( null !== $indexables && ! in_array( $indexable->slug, $indexables, true ) ) {
269 continue;
270 }
271
272 /* translators: 1. Indexable; 2. Site ID */
273 WP_CLI::line( sprintf( esc_html__( 'Adding %1$s mapping for site %2$d…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) );
274
275 $indexable->delete_index();
276 $result = $indexable->put_mapping( 'raw' );
277
278 /**
279 * Fires after CLI put mapping
280 *
281 * @hook ep_cli_put_mapping
282 * @param {Indexable} $indexable Indexable involved in mapping
283 * @param {array} $args CLI command position args
284 * @param {array} $assoc_args CLI command associative args
285 */
286 do_action( 'ep_cli_put_mapping', $indexable, $args, $assoc_args );
287
288 if ( ! is_wp_error( $result ) ) {
289 WP_CLI::success( esc_html__( 'Mapping sent', 'elasticpress' ) );
290 } else {
291 WP_CLI::error(
292 sprintf(
293 /* translators: Error message */
294 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
295 Utils\get_elasticsearch_error_reason( $result->get_error_message() )
296 )
297 );
298 }
299 }
300
301 restore_current_blog();
302 }
303 } else {
304 foreach ( $non_global_indexable_objects as $indexable ) {
305 /**
306 * If user has called out specific indexables to be indexed, only do those
307 */
308 if ( null !== $indexables && ! in_array( $indexable->slug, $indexables, true ) ) {
309 continue;
310 }
311
312 /* translators: Indexable label */
313 WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) );
314
315 $indexable->delete_index();
316 $result = $indexable->put_mapping( 'raw' );
317
318 /**
319 * Fires after CLI put mapping
320 *
321 * @hook ep_cli_put_mapping
322 * @param {Indexable} $indexable Indexable involved in mapping
323 * @param {array} $args CLI command position args
324 * @param {array} $assoc_args CLI command associative args
325 */
326 do_action( 'ep_cli_put_mapping', $indexable, $args, $assoc_args );
327
328 if ( ! is_wp_error( $result ) ) {
329 WP_CLI::success( esc_html__( 'Mapping sent', 'elasticpress' ) );
330 } else {
331 WP_CLI::error(
332 sprintf(
333 /* translators: Error message */
334 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
335 Utils\get_elasticsearch_error_reason( $result->get_error_message() )
336 )
337 );
338 }
339 }
340 }
341
342 /**
343 * Handle global indexables separately
344 */
345 foreach ( $global_indexable_objects as $indexable ) {
346 /**
347 * If user has called out specific indexables to be indexed, only do those
348 */
349 if ( null !== $indexables && ! in_array( $indexable->slug, $indexables, true ) ) {
350 continue;
351 }
352
353 /* translators: Indexable label */
354 WP_CLI::line( sprintf( esc_html__( 'Adding %s mapping…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) );
355
356 $indexable->delete_index();
357 $result = $indexable->put_mapping( 'raw' );
358
359 /**
360 * Fires after CLI put mapping
361 *
362 * @hook ep_cli_put_mapping
363 * @param {Indexable} $indexable Indexable involved in mapping
364 * @param {array} $args CLI command position args
365 * @param {array} $assoc_args CLI command associative args
366 */
367 do_action( 'ep_cli_put_mapping', $indexable, $args, $assoc_args );
368
369 if ( ! is_wp_error( $result ) ) {
370 WP_CLI::success( esc_html__( 'Mapping sent', 'elasticpress' ) );
371 } else {
372 WP_CLI::error(
373 sprintf(
374 /* translators: Error message */
375 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
376 Utils\get_elasticsearch_error_reason( $result->get_error_message() )
377 )
378 );
379 }
380 }
381
382 return true;
383 }
384
385 /**
386 * Return the mapping as a JSON object. If an index is specified, return its mapping only.
387 *
388 * ## OPTIONS
389 *
390 * [--index-name=<index_name>]
391 * : The name of the index for which to return the mapping. If not passed, all mappings will be returned
392 *
393 * [--pretty]
394 * : Use this flag to render a pretty-printed version of the JSON response.
395 *
396 * @subcommand get-mapping
397 * @since 3.6.4, `--pretty` introduced in 4.1.0
398 * @param array $args Positional CLI args.
399 * @param array $assoc_args Associative CLI args.
400 */
401 public function get_mapping( $args, $assoc_args ) {
402 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
403 $index_name = \WP_CLI\Utils\get_flag_value( $assoc_args, 'index-name' );
404 $index_names = (array) ( ! empty( $index_name ) ? $index_name : $this->get_index_names() );
405
406 $path = join( ',', $index_names ) . '/_mapping';
407
408 $response = Elasticsearch::factory()->remote_request( $path );
409
410 $this->print_json_response( $response, $pretty );
411 }
412
413 /**
414 * Return all indices from the cluster as a JSON object.
415 *
416 * ## OPTIONS
417 *
418 * [--pretty]
419 * : Use this flag to render a pretty-printed version of the JSON response.
420 *
421 * @subcommand get-cluster-indices
422 * @since 4.4.0, `--pretty` introduced in 4.1.0
423 * @param array $args Positional CLI args.
424 * @param array $assoc_args Associative CLI args.
425 */
426 public function get_cluster_indices( $args, $assoc_args ) {
427 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
428
429 $cluster_indices = Elasticsearch::factory()->get_cluster_indices();
430
431 $this->pretty_json_encode( $cluster_indices, $pretty );
432 }
433
434 /**
435 * Return all index names as a JSON object.
436 *
437 * ## OPTIONS
438 *
439 * [--pretty]
440 * : Use this flag to render a pretty-printed version of the JSON response.
441 *
442 * [--status=<status>]
443 * : Use this flag to render a pretty-printed version of the JSON response.
444 *
445 * @subcommand get-indices
446 * @since 4.4.0, `--pretty` introduced in 4.1.0, `--status` introduced in 4.5.0
447 * @param array $args Positional CLI args.
448 * @param array $assoc_args Associative CLI args.
449 */
450 public function get_indices( $args, $assoc_args ) {
451 $defaults = [
452 'status' => 'active',
453 ];
454
455 $assoc_args = wp_parse_args( $assoc_args, $defaults );
456 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
457 $index_names = $this->get_index_names( $assoc_args['status'] );
458
459 $this->pretty_json_encode( $index_names, $pretty );
460 }
461
462 /**
463 * Get all index names.
464 *
465 * @param string $status Whether to return active indexables or all registered.
466 * @since 3.6.4, 4.5.0 Added $status
467 * @return array
468 */
469 protected function get_index_names( $status = 'active' ) {
470 return Elasticsearch::factory()->get_index_names( $status );
471 }
472
473 /**
474 * Delete the index for each indexable. !!Warning!! This removes your elasticsearch index(s) for the entire site.
475 *
476 * ## OPTIONS
477 *
478 * [--index-name=<index_name>]
479 * : The name of the index to be deleted. If not passed, all indexes will be deleted
480 *
481 * [--network-wide]
482 * : Force every index on the network to be deleted. `--network-wide` takes an optional argument to limit the number of indices to be deleted where 0 is no limit. For example, `--network-wide=5` would limit to only 5 indices on the network to be deleted.
483 *
484 * [--yes]
485 * : Skip confirmation
486 *
487 * @subcommand delete-index
488 * @since 0.9
489 * @param array $args Positional CLI args.
490 * @param array $assoc_args Associative CLI args.
491 */
492 public function delete_index( $args, $assoc_args ) {
493 $this->connect_check();
494 $this->index_occurring();
495
496 WP_CLI::confirm( esc_html__( 'Are you sure you want to delete your Elasticsearch index?', 'elasticpress' ), $assoc_args );
497
498 // If index name is specified, just delete it and end the command.
499 if ( ! empty( $assoc_args['index-name'] ) ) {
500 $result = Elasticsearch::factory()->delete_index( $assoc_args['index-name'] );
501
502 if ( $result ) {
503 WP_CLI::success( esc_html__( 'Index deleted', 'elasticpress' ) );
504 } else {
505 WP_CLI::error( esc_html__( 'Index delete failed', 'elasticpress' ) );
506 }
507
508 return;
509 }
510
511 $non_global_indexable_objects = Indexables::factory()->get_all( false );
512 $global_indexable_objects = Indexables::factory()->get_all( true );
513
514 if ( isset( $assoc_args['network-wide'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
515 if ( ! is_numeric( $assoc_args['network-wide'] ) ) {
516 $assoc_args['network-wide'] = 0;
517 }
518 $sites = Utils\get_sites( $assoc_args['network-wide'], false );
519
520 foreach ( $sites as $site ) {
521 switch_to_blog( $site['blog_id'] );
522
523 foreach ( $non_global_indexable_objects as $indexable ) {
524 /* translators: 1. Indexable label; 2. Site ID */
525 WP_CLI::line( sprintf( esc_html__( 'Deleting %1$s index for site %2$d…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ), (int) $site['blog_id'] ) );
526
527 $result = $indexable->delete_index();
528
529 if ( $result ) {
530 WP_CLI::success( esc_html__( 'Index deleted', 'elasticpress' ) );
531 } else {
532 WP_CLI::error( esc_html__( 'Delete index failed', 'elasticpress' ) );
533 }
534 }
535
536 restore_current_blog();
537 }
538 } else {
539 foreach ( $non_global_indexable_objects as $indexable ) {
540 /* translators: Index Label (plural) */
541 WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) );
542
543 $result = $indexable->delete_index();
544
545 if ( $result ) {
546 WP_CLI::success( esc_html__( 'Index deleted', 'elasticpress' ) );
547 } else {
548 WP_CLI::error( esc_html__( 'Index delete failed', 'elasticpress' ) );
549 }
550 }
551 }
552
553 foreach ( $global_indexable_objects as $indexable ) {
554 /* translators: Index Label (plural) */
555 WP_CLI::line( sprintf( esc_html__( 'Deleting index for %s…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['plural'] ) ) ) );
556
557 $result = $indexable->delete_index();
558
559 if ( $result ) {
560 WP_CLI::success( esc_html__( 'Index deleted', 'elasticpress' ) );
561 } else {
562 WP_CLI::error( esc_html__( 'Index delete failed', 'elasticpress' ) );
563 }
564 }
565 }
566
567 /**
568 * Recreates the alias index which points to every index in the network.
569 *
570 * Map network alias to every index in the network for every non-global indexable
571 *
572 * @param array $args Positional CLI args.
573 * @subcommand recreate-network-alias
574 * @since 0.9
575 * @param array $assoc_args Associative CLI args.
576 */
577 public function recreate_network_alias( $args, $assoc_args ) {
578 $this->connect_check();
579 $this->index_occurring();
580
581 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
582 WP_CLI::error( esc_html__( 'ElasticPress is not network activated.', 'elasticpress' ) );
583 }
584
585 $indexables = Indexables::factory()->get_all( false );
586
587 foreach ( $indexables as $indexable ) {
588 /* translators: Index Label */
589 WP_CLI::line( sprintf( esc_html__( 'Recreating %s network alias…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) );
590
591 $indexable->delete_network_alias();
592
593 $create_result = $this->create_network_alias_helper( $indexable );
594
595 if ( $create_result ) {
596 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
597 } else {
598 WP_CLI::error( esc_html__( 'An error occurred', 'elasticpress' ) );
599 }
600 }
601 }
602
603 /**
604 * A WP-CLI wrapper to run `Autosuggest::epio_send_autosuggest_public_request()`.
605 *
606 * @param array $args Positional CLI args.
607 * @param array $assoc_args Associative CLI args.
608 * @subcommand epio-set-autosuggest
609 * @since 3.5.x
610 */
611 public function epio_set_autosuggest( $args, $assoc_args ) {
612 $autosuggest_feature = Features::factory()->get_registered_feature( 'autosuggest' );
613
614 if ( empty( $autosuggest_feature ) || ! $autosuggest_feature->is_active() ) {
615 WP_CLI::error( esc_html__( 'Autosuggest is not enabled.', 'elasticpress' ) );
616 }
617
618 add_action( 'ep_epio_wp_cli_set_autosuggest', [ $autosuggest_feature, 'epio_send_autosuggest_public_request' ] );
619
620 do_action( 'ep_epio_wp_cli_set_autosuggest', $args, $assoc_args );
621
622 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
623 }
624
625 /**
626 * A WP-CLI wrapper to run `Autosuggest::post_deactivation()`.
627 *
628 * @subcommand epio-reset-autosuggest
629 * @since 5.3.2
630 */
631 public function epio_reset_autosuggest() {
632 Features::factory()->get_registered_feature( 'autosuggest' )->post_deactivation();
633
634 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
635 }
636
637 /**
638 * Helper method for creating the network alias for an indexable
639 *
640 * @param Indexable $indexable Instance of indexable.
641 * @since 0.9
642 * @return array|bool
643 */
644 private function create_network_alias_helper( Indexable $indexable ) {
645 $sites = Utils\get_sites( 0, true );
646 $indexes = [];
647
648 foreach ( $sites as $site ) {
649 switch_to_blog( $site['blog_id'] );
650
651 $indexes[] = $indexable->get_index_name();
652
653 restore_current_blog();
654 }
655
656 return $indexable->create_network_alias( $indexes );
657 }
658
659 /**
660 * Properly clean up when receiving SIGINT on indexing
661 *
662 * @param int $signal_no Signal number
663 * @since 3.3
664 */
665 public function delete_transient_on_int( $signal_no ) {
666 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::delete_transient_on_int' );
667 Utility::delete_transient_on_int( $signal_no );
668 }
669
670 /**
671 * Index all posts for a site or network wide.
672 *
673 * ## OPTIONS
674 *
675 * [--network-wide]
676 * : Force indexing on all the blogs in the network. `--network-wide` takes an optional argument to limit the number of blogs to be indexed across where 0 is no limit. For example, `--network-wide=5` would limit indexing to only 5 blogs on the network
677 *
678 * [--setup]
679 * : Clear the index first and re-send the put mapping. Use `--yes` to skip the confirmation
680 *
681 * [--force]
682 * : Stop any ongoing sync
683 *
684 * [--per-page=<per_page_number>]
685 * : Determine the amount of posts to be indexed per bulk index (or cycle)
686 *
687 * [--nobulk]
688 * : Disable bulk indexing
689 *
690 * [--static-bulk]
691 * : Do not use dynamic bulk requests, i.e., send only one request per batch of documents.
692 *
693 * [--show-errors]
694 * : Show all errors
695 *
696 * [--show-bulk-errors]
697 * : Display the error message returned from Elasticsearch when a post fails to index using the /_bulk endpoint
698 *
699 * [--show-nobulk-errors]
700 * : Display the error message returned from Elasticsearch when a post fails to index while not using the /_bulk endpoint
701 *
702 * [--stop-on-error]
703 * : Stop indexing if an error is encountered and display the error.
704 *
705 * [--offset=<offset_number>]
706 * : Skip the first n posts (don't forget to remove the `--setup` flag when resuming or the index will be emptied before starting again).
707 *
708 * [--indexables=<indexables>]
709 * : Specify the Indexable(s) which will be indexed
710 *
711 * [--post-type=<post_types>]
712 * : Specify which post types will be indexed (by default: all indexable post types are indexed). For example, `--post-type="my_custom_post_type"` would limit indexing to only posts from the post type "my_custom_post_type". Accepts multiple post types separated by comma
713 *
714 * [--include=<IDs>]
715 * : Choose which object IDs to include in the index
716 *
717 * [--post-ids=<IDs>]
718 * : Choose which post_ids to include when indexing the Posts Indexable (deprecated)
719 *
720 * [--upper-limit-object-id=<ID>]
721 * : Upper limit of a range of IDs to be indexed. If indexing IDs from 30 to 45, this should be 45
722 *
723 * [--lower-limit-object-id=<ID>]
724 * : Lower limit of a range of IDs to be indexed. If indexing IDs from 30 to 45, this should be 30
725 *
726 * [--ep-host=<host>]
727 * : Custom Elasticsearch host
728 *
729 * [--ep-prefix=<prefix>]
730 * : Custom ElasticPress prefix
731 *
732 * [--yes]
733 * : Skip confirmation needed by `--setup`
734 *
735 * @param array $args Positional CLI args.
736 * @since 4.4.0
737 * @param array $assoc_args Associative CLI args.
738 */
739 public function sync( $args, $assoc_args ) {
740 $setup_option = \WP_CLI\Utils\get_flag_value( $assoc_args, 'setup', false );
741 $force_option = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false );
742
743 if ( $setup_option ) {
744 $message = sprintf(
745 /* translators: ElasticPress.io or Elasticsearch */
746 esc_html__( 'Syncing with the --setup option will delete your existing index in %s. Are you sure you want to delete your Elasticsearch index', 'elasticpress' ),
747 Utils\is_epio() ? 'ElasticPress.io' : 'Elasticsearch'
748 );
749 WP_CLI::confirm( $message, $assoc_args );
750 }
751
752 if ( $force_option ) {
753 WP_CLI::confirm( esc_html__( 'Are you sure you want to stop any other ongoing sync?', 'elasticpress' ), $assoc_args );
754 }
755
756 if ( ! function_exists( 'pcntl_signal' ) ) {
757 WP_CLI::warning( esc_html__( 'Function pcntl_signal not available. Make sure to run `wp elasticpress clear-sync` in case the process is killed.', 'elasticpress' ) );
758 } else {
759 declare( ticks = 1 );
760 pcntl_signal( SIGINT, [ Utility::class, 'delete_transient_on_int' ] );
761 }
762
763 $this->maybe_change_host( $assoc_args );
764 $this->maybe_change_index_prefix( $assoc_args );
765 $this->connect_check();
766
767 if ( $force_option ) {
768 $this->clear_sync();
769 } else {
770 $this->index_occurring();
771 }
772
773 $indexables = null;
774
775 if ( ! empty( $assoc_args['indexables'] ) ) {
776 $indexables = explode( ',', str_replace( ' ', '', $assoc_args['indexables'] ) );
777 }
778
779 /**
780 * Prior to the index command invoking
781 * Useful for deregistering filters/actions that occur during a query request
782 *
783 * @since 1.4.1
784 */
785 /**
786 * Fires before starting a CLI index
787 *
788 * @hook ep_wp_cli_pre_index
789 * @param {array} $args CLI command position args
790 * @param {array} $assoc_args CLI command associative args
791 */
792 do_action( 'ep_wp_cli_pre_index', $args, $assoc_args );
793
794 Utility::timer_start();
795
796 add_action( 'ep_sync_put_mapping', [ Utility::class, 'stop_on_failed_mapping' ], 10, 3 );
797 add_action( 'ep_sync_put_mapping', [ Utility::class, 'call_ep_cli_put_mapping' ], 10, 2 );
798 add_action( 'ep_index_batch_new_attempt', [ Utility::class, 'should_interrupt_sync' ] );
799
800 $no_bulk = ! empty( $assoc_args['nobulk'] );
801 $static_bulk = \WP_CLI\Utils\get_flag_value( $assoc_args, 'static-bulk', null );
802 $network_wide = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network-wide', null );
803
804 $index_args = [
805 'method' => 'cli',
806 'total_attempts' => 1,
807 'indexables' => $indexables,
808 'put_mapping' => $setup_option,
809 'output_method' => [ $this, 'index_output' ],
810 'network_wide' => $network_wide,
811 'nobulk' => $no_bulk,
812 'offset' => ( ! empty( $assoc_args['offset'] ) ) ? absint( $assoc_args['offset'] ) : 0,
813 'static_bulk' => $static_bulk,
814 ];
815
816 $index_args['stop_on_error'] = WP_CLI\Utils\get_flag_value( $assoc_args, 'stop-on-error', false );
817
818 $show_errors = $index_args['stop_on_error'] ||
819 WP_CLI\Utils\get_flag_value( $assoc_args, 'show-errors', false ) ||
820 ( WP_CLI\Utils\get_flag_value( $assoc_args, 'show-bulk-errors', false ) && ! $no_bulk ) ||
821 ( WP_CLI\Utils\get_flag_value( $assoc_args, 'show-nobulk-errors', false ) && $no_bulk );
822
823 if ( $show_errors ) {
824 $index_args['show_errors'] = true;
825 }
826
827 if ( ! empty( $assoc_args['post-ids'] ) ) {
828 $assoc_args['include'] = $assoc_args['post-ids'];
829 }
830
831 if ( ! empty( $assoc_args['include'] ) ) {
832 $include = explode( ',', str_replace( ' ', '', $assoc_args['include'] ) );
833 $index_args['include'] = array_map( 'absint', $include );
834 $index_args['per_page'] = count( $index_args['include'] );
835 }
836
837 if ( ! empty( $assoc_args['per-page'] ) ) {
838 $index_args['per_page'] = absint( $assoc_args['per-page'] );
839 }
840
841 if ( ! empty( $assoc_args['post-type'] ) ) {
842 $index_args['post_type'] = explode( ',', $assoc_args['post-type'] );
843 $index_args['post_type'] = array_map( 'trim', $index_args['post_type'] );
844 // If post-type was passed, only index the Post indexable.
845 $index_args['indexables'] = [ 'post' ];
846 }
847
848 if ( ! empty( $assoc_args['upper-limit-object-id'] ) && is_numeric( $assoc_args['upper-limit-object-id'] ) ) {
849 $index_args['upper_limit_object_id'] = absint( $assoc_args['upper-limit-object-id'] );
850 }
851
852 if ( ! empty( $assoc_args['lower-limit-object-id'] ) && is_numeric( $assoc_args['lower-limit-object-id'] ) ) {
853 $index_args['lower_limit_object_id'] = absint( $assoc_args['lower-limit-object-id'] );
854 }
855
856 \ElasticPress\IndexHelper::factory()->full_index( $index_args );
857
858 remove_action( 'ep_sync_put_mapping', [ Utility::class, 'stop_on_failed_mapping' ] );
859 remove_action( 'ep_sync_put_mapping', [ Utility::class, 'call_ep_cli_put_mapping' ], 10, 2 );
860 remove_action( 'ep_index_batch_new_attempt', [ Utility::class, 'should_interrupt_sync' ] );
861
862 $sync_time_in_ms = Utility::timer_stop();
863
864 /**
865 * Fires after executing a CLI index
866 *
867 * @hook ep_wp_cli_after_index
868 * @param {array} $args CLI command position args
869 * @param {array} $assoc_args CLI command associative args
870 *
871 * @since 3.5.5
872 */
873 do_action( 'ep_wp_cli_after_index', $args, $assoc_args );
874
875 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Total time elapsed: ', 'elasticpress' ) . '%N' . Utility::timer_format( $sync_time_in_ms ) ) );
876
877 Utility::delete_transient();
878
879 WP_CLI::success( esc_html__( 'Done!', 'elasticpress' ) );
880 }
881
882 /**
883 * Ping the Elasticsearch server and retrieve a status.
884 *
885 * @since 0.9.1
886 */
887 public function status() {
888 $this->connect_check();
889
890 $registered_index_names = $this->get_index_names();
891
892 $response_cat_indices = Elasticsearch::factory()->remote_request( '_cat/indices?format=json' );
893
894 if ( is_wp_error( $response_cat_indices ) ) {
895 WP_CLI::error( implode( "\n", $response_cat_indices->get_error_messages() ) );
896 }
897
898 $indexes_from_cat_indices_api = json_decode( wp_remote_retrieve_body( $response_cat_indices ), true );
899
900 if ( is_array( $indexes_from_cat_indices_api ) ) {
901 $indexes_from_cat_indices_api = wp_list_pluck( $indexes_from_cat_indices_api, 'index' );
902
903 $index_names = array_intersect( $registered_index_names, $indexes_from_cat_indices_api );
904 } else {
905 WP_CLI::error( esc_html__( 'Failed to return status.', 'elasticpress' ) );
906 }
907
908 $index_names_imploded = implode( ',', $index_names );
909
910 $request = Elasticsearch::factory()->remote_request( $index_names_imploded . '/_recovery/?pretty' );
911
912 if ( is_wp_error( $request ) ) {
913 WP_CLI::error( implode( "\n", $request->get_error_messages() ) );
914 }
915
916 $body = wp_remote_retrieve_body( $request );
917 WP_CLI::line( '' );
918 WP_CLI::line( '====== Status ======' );
919 // phpcs:disable
920 WP_CLI::line( print_r( $body, true ) );
921 // phpcs:enable
922 WP_CLI::line( '====== End Status ======' );
923 }
924
925 /**
926 * Get stats on the current index.
927 *
928 * @since 0.9.2
929 */
930 public function stats() {
931 $this->connect_check();
932
933 $registered_index_names = $this->get_index_names();
934
935 $response_cat_indices = Elasticsearch::factory()->remote_request( '_cat/indices?format=json' );
936
937 if ( is_wp_error( $response_cat_indices ) ) {
938 WP_CLI::error( implode( "\n", $response_cat_indices->get_error_messages() ) );
939 }
940
941 $indexes_from_cat_indices_api = json_decode( wp_remote_retrieve_body( $response_cat_indices ), true );
942
943 if ( is_array( $indexes_from_cat_indices_api ) ) {
944 $indexes_from_cat_indices_api = wp_list_pluck( $indexes_from_cat_indices_api, 'index' );
945
946 $index_names = array_intersect( $registered_index_names, $indexes_from_cat_indices_api );
947 } else {
948 WP_CLI::error( esc_html__( 'Failed to return stats.', 'elasticpress' ) );
949 }
950
951 $index_names_imploded = implode( ',', $index_names );
952
953 Elasticsearch::factory()->refresh_indices();
954 $request = Elasticsearch::factory()->remote_request( $index_names_imploded . '/_stats/' );
955
956 if ( is_wp_error( $request ) ) {
957 WP_CLI::error( implode( "\n", $request->get_error_messages() ) );
958 }
959 $body = json_decode( wp_remote_retrieve_body( $request ), true );
960
961 foreach ( $registered_index_names as $index_name ) {
962 $this->render_stats( $index_name, $body );
963 }
964 }
965
966 /**
967 * Provide better error messaging for common connection errors
968 *
969 * @since 0.9.3
970 */
971 private function connect_check() {
972 $host = Utils\get_host();
973
974 if ( empty( $host ) ) {
975 WP_CLI::error( esc_html__( 'Elasticsearch host is not set.', 'elasticpress' ) );
976 } elseif ( ! Elasticsearch::factory()->get_elasticsearch_version( true ) ) {
977 WP_CLI::error( esc_html__( 'Could not connect to Elasticsearch.', 'elasticpress' ) );
978 }
979 }
980
981 /**
982 * Error out if index is already occurring
983 *
984 * @since 3.0
985 */
986 private function index_occurring() {
987 if ( Utils\is_indexing() ) {
988 WP_CLI::error( esc_html__( 'An index is already occurring. Try again later.', 'elasticpress' ) );
989 }
990 }
991
992 /**
993 * Delete transient that indicates indexing is occurring
994 *
995 * @since 3.1
996 */
997 private function delete_transient() {
998 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::delete_transient()' );
999 Utility::delete_transient();
1000 }
1001
1002 /**
1003 * Clear a sync/index process.
1004 *
1005 * If an index was stopped prematurely and won't start again, this will clear this cached data such that a new index can start.
1006 *
1007 * @subcommand clear-sync
1008 * @alias delete-transient
1009 * @since 4.4.0
1010 */
1011 public function clear_sync() {
1012 /**
1013 * Fires before the CLI `clear-sync` command is executed.
1014 *
1015 * @hook ep_cli_before_clear_index
1016 *
1017 * @since 3.5.5
1018 */
1019 do_action( 'ep_cli_before_clear_index' );
1020
1021 Utility::delete_transient();
1022
1023 /**
1024 * Fires after the CLI `clear-sync` command is executed.
1025 *
1026 * @hook ep_cli_after_clear_index
1027 *
1028 * @since 3.5.5
1029 */
1030 do_action( 'ep_cli_after_clear_index' );
1031
1032 WP_CLI::log( esc_html__( 'Sync cleared.', 'elasticpress' ) );
1033 }
1034
1035 /**
1036 * Returns the status of an ongoing index operation in JSON array.
1037 *
1038 * Returns the status of an ongoing index operation in JSON array with the following fields:
1039 * indexing | boolean | True if index operation is ongoing or false
1040 * method | string | 'cli', 'web' or 'none'
1041 * items_indexed | integer | Total number of items indexed
1042 * total_items | integer | Total number of items indexed or -1 if not yet determined
1043 *
1044 * ## OPTIONS
1045 *
1046 * [--pretty]
1047 * : Use this flag to render a pretty-printed version of the JSON response.
1048 *
1049 * @subcommand get-ongoing-sync-status
1050 * @since 3.5.1, `--pretty` introduced in 4.1.0
1051 * @param array $args Positional CLI args.
1052 * @param array $assoc_args Associative CLI args.
1053 */
1054 public function get_ongoing_sync_status( $args, $assoc_args ) {
1055 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1056 $indexing_status = Utils\get_indexing_status();
1057
1058 if ( empty( $indexing_status ) ) {
1059 $indexing_status = [
1060 'indexing' => false,
1061 'method' => 'none',
1062 'items_indexed' => 0,
1063 'total_items' => -1,
1064 ];
1065 }
1066
1067 $this->pretty_json_encode( $indexing_status, $pretty );
1068 }
1069
1070 /**
1071 * Returns a JSON array with the results of the last index (if present) or an empty array.
1072 *
1073 * ## OPTIONS
1074 *
1075 * [--pretty]
1076 * : Use this flag to render a pretty-printed version of the JSON response.
1077 *
1078 * @subcommand get-last-sync
1079 * @alias get-last-index
1080 * @since 4.2.0
1081 * @param array $args Positional CLI args.
1082 * @param array $assoc_args Associative CLI args.
1083 */
1084 public function get_last_sync( $args, $assoc_args ) {
1085 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1086 $last_sync = \ElasticPress\IndexHelper::factory()->get_last_sync();
1087
1088 $this->pretty_json_encode( $last_sync, $pretty );
1089 }
1090
1091 /**
1092 * Returns a JSON array with the results of the last CLI sync (if present) or an empty array.
1093 *
1094 * ## OPTIONS
1095 *
1096 * [--clear]
1097 * : Clear the `ep_last_cli_index` option.
1098 *
1099 * [--pretty]
1100 * : Use this flag to render a pretty-printed version of the JSON response.
1101 *
1102 * @subcommand get-last-cli-sync
1103 * @since 4.4.0, `--pretty` introduced in 4.1.0
1104 * @param array $args Positional CLI args.
1105 * @param array $assoc_args Associative CLI args.
1106 */
1107 public function get_last_cli_sync( $args, $assoc_args ) {
1108 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1109 $last_sync = Utils\get_option( 'ep_last_cli_index', array() );
1110
1111 if ( isset( $assoc_args['clear'] ) ) {
1112 Utils\delete_option( 'ep_last_cli_index' );
1113 }
1114
1115 $this->pretty_json_encode( $last_sync, $pretty );
1116 }
1117
1118
1119 /**
1120 * maybe change Elastic host on the fly
1121 *
1122 * @param array $assoc_args Associative CLI args.
1123 *
1124 * @since 3.4
1125 */
1126 private function maybe_change_host( $assoc_args ) {
1127 if ( isset( $assoc_args['ep-host'] ) ) {
1128 add_filter(
1129 'ep_host',
1130 function () use ( $assoc_args ) {
1131 return $assoc_args['ep-host'];
1132 }
1133 );
1134 }
1135 }
1136
1137 /**
1138 * maybe change index prefix on the fly
1139 *
1140 * @param array $assoc_args Associative CLI args.
1141 *
1142 * @since 3.4
1143 */
1144 private function maybe_change_index_prefix( $assoc_args ) {
1145 if ( isset( $assoc_args['ep-prefix'] ) ) {
1146 add_filter(
1147 'ep_index_prefix',
1148 function ( $prefix ) use ( $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
1149 return $assoc_args['ep-prefix'];
1150 }
1151 );
1152 }
1153 }
1154
1155 /**
1156 * Check if sync should be interrupted
1157 *
1158 * @since 3.5.2
1159 */
1160 public function should_interrupt_sync() {
1161 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::should_interrupt_sync' );
1162 Utility::should_interrupt_sync();
1163 }
1164
1165 /**
1166 * Stop the Sync operation started from the dashboard.
1167 *
1168 * @subcommand stop-sync
1169 * @since 4.4.0
1170 * @param array $args Positional CLI args.
1171 * @param array $assoc_args Associative CLI args.
1172 */
1173 public function stop_sync( $args, $assoc_args ) {
1174 $indexing_status = \ElasticPress\Utils\get_indexing_status();
1175
1176 if ( empty( \ElasticPress\Utils\get_indexing_status() ) ) {
1177 WP_CLI::warning( esc_html__( 'There is no indexing operation running.', 'elasticpress' ) );
1178 } else {
1179 WP_CLI::line( esc_html__( 'Stopping indexing…', 'elasticpress' ) );
1180
1181 if ( isset( $indexing_status['method'] ) && 'cli' === $indexing_status['method'] ) {
1182 set_transient( 'ep_wpcli_sync_interrupted', true, MINUTE_IN_SECONDS );
1183 } else {
1184 set_transient( 'ep_sync_interrupted', true, MINUTE_IN_SECONDS );
1185 }
1186
1187 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1188 }
1189 }
1190
1191 /**
1192 * Set the algorithm version.
1193 *
1194 * Set the algorithm version through the `ep_search_algorithm_version` option,
1195 * that will be used by the filter with same name.
1196 * Delete the option if `--default` is passed.
1197 *
1198 * ## OPTIONS
1199 *
1200 * [--version=<version>]
1201 * : Version name
1202 *
1203 * [--default]
1204 * : Use to set the default version
1205 *
1206 * @subcommand set-algorithm-version
1207 *
1208 * @since 3.5.4
1209 * @param array $args Positional CLI args.
1210 * @param array $assoc_args Associative CLI args.
1211 */
1212 public function set_search_algorithm_version( $args, $assoc_args ) {
1213 /**
1214 * Fires before the algorithm version is changed via WP-CLI.
1215 *
1216 * @hook ep_cli_before_set_search_algorithm_version
1217 * @param {array} $args CLI command position args
1218 * @param {array} $assoc_args CLI command associative args
1219 *
1220 * @since 3.5.5
1221 */
1222 do_action( 'ep_cli_before_set_search_algorithm_version', $args, $assoc_args );
1223
1224 if ( empty( $assoc_args['version'] ) && ! isset( $assoc_args['default'] ) ) {
1225 WP_CLI::error( esc_html__( 'This command expects a version number or the --default flag.', 'elasticpress' ) );
1226 }
1227
1228 if ( ! empty( $assoc_args['default'] ) ) {
1229 Utils\delete_option( 'ep_search_algorithm_version' );
1230 } else {
1231 Utils\update_option( 'ep_search_algorithm_version', $assoc_args['version'] );
1232 }
1233
1234 /**
1235 * Fires after the algorithm version is changed via WP-CLI.
1236 *
1237 * @hook ep_cli_after_set_search_algorithm_version
1238 * @param {array} $args CLI command position args
1239 * @param {array} $assoc_args CLI command associative args
1240 *
1241 * @since 3.5.5
1242 */
1243 do_action( 'ep_cli_after_set_search_algorithm_version', $args, $assoc_args );
1244
1245 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1246 }
1247
1248 /**
1249 * Get the algorithm version.
1250 *
1251 * Get the value of the `ep_search_algorithm_version` option, or
1252 * `default` if empty.
1253 *
1254 * @subcommand get-algorithm-version
1255 *
1256 * @since 3.5.4
1257 * @param array $args Positional CLI args.
1258 * @param array $assoc_args Associative CLI args.
1259 */
1260 public function get_search_algorithm_version( $args, $assoc_args ) {
1261 $value = Utils\get_option( 'ep_search_algorithm_version', '' );
1262
1263 if ( empty( $value ) ) {
1264 WP_CLI::line( 'default' );
1265 } else {
1266 WP_CLI::line( $value );
1267 }
1268 }
1269
1270 /**
1271 * Custom get_transient to WP-CLI env.
1272 *
1273 * We are using the direct SQL query instead of
1274 * the regular function call to retrieve the updated
1275 * value to stop the sync. Otherwise, we always get
1276 * false after the command is running even when the value
1277 * is updated.
1278 *
1279 * @since 3.5.2
1280 * @param mixed $pre_transient The default value.
1281 * @param string $transient Transient name.
1282 * @return true|null
1283 */
1284 public function custom_get_transient( $pre_transient, $transient ) {
1285 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::custom_get_transient' );
1286 return Utility::custom_get_transient( $pre_transient, $transient );
1287 }
1288
1289 /**
1290 * Utility function to render Stats for a given index.
1291 *
1292 * @since 3.5.6
1293 * @param string $current_index The index name.
1294 * @param array $body The response body.
1295 * @return void
1296 */
1297 protected function render_stats( $current_index, $body ) {
1298 if ( isset( $body['indices'][ $current_index ] ) ) {
1299 WP_CLI::log( '====== Stats for: ' . $current_index . ' ======' );
1300 WP_CLI::log( 'Documents: ' . $body['indices'][ $current_index ]['primaries']['docs']['count'] );
1301 WP_CLI::log( 'Index Size: ' . size_format( $body['indices'][ $current_index ]['primaries']['store']['size_in_bytes'], 2 ) );
1302 WP_CLI::log( 'Index Size (including replicas): ' . size_format( $body['indices'][ $current_index ]['total']['store']['size_in_bytes'], 2 ) );
1303 WP_CLI::log( '====== End Stats ======' );
1304 } else {
1305 WP_CLI::warning( $current_index . ' is not currently indexed.' );
1306 }
1307 }
1308
1309 /**
1310 * Function used to output messages coming from IndexHelper
1311 *
1312 * @param array $message Message data
1313 * @param array $args Args sent and processed by IndexHelper
1314 * @param array $index_meta Current index state
1315 * @param string $context Context of the message being outputted
1316 */
1317 public function index_output( $message, $args, $index_meta, $context ) {
1318 static $time_elapsed = 0, $counter = 0;
1319
1320 switch ( $message['status'] ) {
1321 case 'success':
1322 WP_CLI::success( $message['message'] );
1323 break;
1324
1325 case 'warning':
1326 if ( empty( $args['show_errors'] ) ) {
1327 return;
1328 }
1329
1330 WP_CLI::warning( $message['message'] );
1331 break;
1332
1333 case 'error':
1334 $this->clear_sync();
1335 WP_CLI::error( $message['message'] );
1336 break;
1337
1338 default:
1339 WP_CLI::log( $message['message'] );
1340 break;
1341 }
1342
1343 if ( 'index_next_batch' === $context ) {
1344 ++$counter;
1345 if ( ( $counter % 10 ) === 0 ) {
1346 $time_elapsed_diff = $time_elapsed > 0 ? ' (+' . (string) ( Utility::timer_stop() - $time_elapsed ) . ')' : '';
1347 $time_elapsed = Utility::timer_stop( 2 );
1348 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Time elapsed: ', 'elasticpress' ) . '%N' . Utility::timer_format( $time_elapsed ) . $time_elapsed_diff ) );
1349
1350 $current_memory = memory_get_usage() / 1024 / 1024;
1351 $current_memory = ( $current_memory > 1000 ) ? round( $current_memory / 1024, 2 ) . 'gb' : round( $current_memory, 2 ) . 'mb';
1352 $peak_memory = memory_get_peak_usage() / 1024 / 1024;
1353 $peak_memory = ( $peak_memory > 1000 ) ? round( $peak_memory / 1024, 2 ) . 'gb' : round( $peak_memory, 2 ) . 'mb';
1354 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Memory Usage: ', 'elasticpress' ) . '%N' . $current_memory . ' (Peak: ' . $peak_memory . ')' ) );
1355 }
1356 }
1357 }
1358
1359 /**
1360 * If put_mapping fails while indexing, stop the index process.
1361 *
1362 * @param array $index_meta Index meta info
1363 * @param Indexable $indexable Indexable object
1364 * @param bool $result Whether the request was successful or not
1365 */
1366 public function stop_on_failed_mapping( $index_meta, $indexable, $result ) {
1367 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::stop_on_failed_mapping' );
1368 Utility::stop_on_failed_mapping( $index_meta, $indexable, $result );
1369 }
1370
1371 /**
1372 * Ties the `ep_cli_put_mapping` action to `ep_sync_put_mapping`.
1373 *
1374 * @since 4.0.0
1375 *
1376 * @param array $index_meta Index meta information
1377 * @param Indexable $indexable Indexable object
1378 * @return void
1379 */
1380 public function call_ep_cli_put_mapping( $index_meta, $indexable ) {
1381 _deprecated_function( __METHOD__, '4.5.0', '\ElasticPress\Command\Utility::call_ep_cli_put_mapping' );
1382 Utility::call_ep_cli_put_mapping( $index_meta, $indexable );
1383 }
1384
1385 /**
1386 * Send a HTTP request to Elasticsearch
1387 *
1388 * ## OPTIONS
1389 *
1390 * <path>
1391 * : Path of the request. Example: `_cat/indices`
1392 *
1393 * [--method=<method>]
1394 * : HTTP Method (GET, POST, etc.)
1395 *
1396 * [--body=<json-body>]
1397 * : Request body
1398 *
1399 * [--debug-http-request]
1400 * : Enable debugging
1401 *
1402 * [--pretty]
1403 * : Use this flag to render a pretty-printed version of the JSON response.
1404 *
1405 * @subcommand request
1406 *
1407 * @since 3.6.6, `--pretty` introduced in 4.1.0
1408 *
1409 * @param array $args Positional CLI args.
1410 * @param array $assoc_args Associative CLI args.
1411 */
1412 public function request( $args, $assoc_args ) {
1413 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1414 $path = $args[0];
1415 $method = isset( $assoc_args['method'] ) ? $assoc_args['method'] : 'GET';
1416 $body = isset( $assoc_args['body'] ) ? $assoc_args['body'] : '';
1417 $request_args = [
1418 'method' => $method,
1419 ];
1420 if ( 'GET' !== $method && ! empty( $body ) ) {
1421 $request_args['body'] = $body;
1422 }
1423
1424 $this->maybe_add_http_api_debug_filter( $assoc_args );
1425 $response = Elasticsearch::factory()->remote_request( $path, $request_args, [], 'wp_cli_request' );
1426
1427 if ( is_wp_error( $response ) ) {
1428 WP_CLI::error( $response->get_error_message() );
1429 }
1430
1431 $this->print_json_response( $response, $pretty );
1432 }
1433
1434 /**
1435 * Reset all ElasticPress settings stored in WP options and transients.
1436 *
1437 * This command will not delete any index or content stored in Elasticsearch but will force users to go through the installation process again.
1438 *
1439 * ## OPTIONS
1440 *
1441 * [--yes]
1442 * : Skip confirmation
1443 *
1444 * @subcommand settings-reset
1445 *
1446 * @since 4.2.0
1447 *
1448 * @param array $args Positional CLI args.
1449 * @param array $assoc_args Associative CLI args.
1450 */
1451 public function settings_reset( $args, $assoc_args ) {
1452 WP_CLI::confirm( esc_html__( 'Are you sure you want to delete all ElasticPress settings?', 'elasticpress' ), $assoc_args );
1453
1454 define( 'EP_MANUAL_SETTINGS_RESET', true );
1455 include EP_PATH . '/uninstall.php';
1456
1457 WP_CLI::line( esc_html__( 'Settings deleted.', 'elasticpress' ) );
1458 }
1459
1460
1461 /**
1462 * Print an HTTP response.
1463 *
1464 * @since 4.1.0
1465 * @param array $response HTTP Response.
1466 * @param boolean $pretty Whether the JSON response should be formatted or not.
1467 */
1468 protected function print_json_response( $response, $pretty ) {
1469 $response_body = wp_remote_retrieve_body( $response );
1470
1471 $content_type = wp_remote_retrieve_header( $response, 'Content-Type' );
1472
1473 if ( ! $pretty || ! preg_match( '/json/', $content_type ) ) {
1474 WP_CLI::line( $response_body );
1475 return;
1476 }
1477
1478 // Re-encode the JSON to add space formatting
1479 $response_body_obj = json_decode( $response_body );
1480
1481 $this->pretty_json_encode( $response_body_obj, JSON_PRETTY_PRINT );
1482 }
1483
1484 /**
1485 * Output a JSON object. Conditionally format it before doing so.
1486 *
1487 * @since 4.1.0
1488 * @param array $json_obj The JSON object or array.
1489 * @param boolean $pretty_print_flag Whether it should or not be formatted.
1490 */
1491 protected function pretty_json_encode( $json_obj, $pretty_print_flag ) {
1492 $flag = $pretty_print_flag ? JSON_PRETTY_PRINT : 0;
1493 WP_CLI::line( wp_json_encode( $json_obj, $flag ) );
1494 }
1495
1496 /**
1497 * Gets the Instant Results search template.
1498 *
1499 * ## OPTIONS
1500 *
1501 * [--pretty]
1502 * : Use this flag to render a pretty-printed version of the JSON response.
1503 *
1504 * @since 4.5.0
1505 * @param array $args Positional CLI args.
1506 * @param array $assoc_args Associative CLI args.
1507 *
1508 * @subcommand get-search-template
1509 */
1510 public function get_search_template( $args, $assoc_args ) {
1511 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1512 $instant_results = Features::factory()->get_registered_feature( 'instant-results' );
1513 $template = json_decode( $instant_results->epio_get_search_template() );
1514
1515 $this->pretty_json_encode( $template, $pretty );
1516 }
1517
1518 /**
1519 * Saves the Instant Results search template to EPIO.
1520 *
1521 * ## OPTIONS
1522 *
1523 * [--network-wide]
1524 * : Save the template for all sites in the network if plugin is network activated. Otherwise, save the template for the current site only.
1525 *
1526 * @since 4.5.0
1527 * @param array $args Positional CLI args.
1528 * @param array $assoc_args Associative CLI args.
1529 * @subcommand put-search-template
1530 */
1531 public function put_search_template( $args, $assoc_args ) {
1532 $network_wide = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network-wide', false );
1533 $instant_results = Features::factory()->get_registered_feature( 'instant-results' );
1534
1535 $instant_results->epio_save_site_search_template( $network_wide );
1536
1537 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1538 }
1539
1540 /**
1541 * Deletes the Instant Results search template.
1542 *
1543 * ## OPTIONS
1544 *
1545 * [--network-wide]
1546 * : Delete the template for all sites in the network if plugin is network activated. Otherwise, delete the template for the current site only.
1547 *
1548 * @since 4.5.0
1549 * @param array $args Positional CLI args.
1550 * @param array $assoc_args Associative CLI args.
1551 * @subcommand delete-search-template
1552 */
1553 public function delete_search_template( $args, $assoc_args ) {
1554 $network_wide = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network-wide', false );
1555 $instant_results = Features::factory()->get_registered_feature( 'instant-results' );
1556
1557 $instant_results->epio_delete_site_search_template( $network_wide );
1558
1559 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1560 }
1561
1562 /**
1563 * Get an index settings
1564 *
1565 * ## OPTIONS
1566 *
1567 * <index_name>
1568 * : Index name
1569 *
1570 * [--pretty]
1571 * : Use this flag to render a pretty-printed version of the JSON response.
1572 *
1573 * @subcommand get-index-settings
1574 *
1575 * @since 4.7.0
1576 *
1577 * @param array $args Positional CLI args.
1578 * @param array $assoc_args Associative CLI args.
1579 */
1580 public function get_index_settings( $args, $assoc_args ) {
1581 $response = Elasticsearch::factory()->get_index_settings( $args[0], true );
1582 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1583
1584 $this->pretty_json_encode( $response, $pretty );
1585 }
1586
1587 /**
1588 * Get a specific content in Elasticsearch
1589 *
1590 * ## OPTIONS
1591 *
1592 * <indexable>
1593 * : Indexable slug. Example: `post`
1594 *
1595 * <ID>
1596 * : Content ID
1597 *
1598 * [--debug-http-request]
1599 * : Enable debugging
1600 *
1601 * [--pretty]
1602 * : Use this flag to render a pretty-printed version of the JSON response.
1603 *
1604 * @since 4.7.0
1605 *
1606 * @param array $args Positional CLI args.
1607 * @param array $assoc_args Associative CLI args.
1608 */
1609 public function get( $args, $assoc_args ) {
1610 $indexables = Indexables::factory();
1611
1612 $indexable = $indexables->get( $args[0] );
1613 if ( ! $indexable || ! $indexables->is_active( $args[0] ) ) {
1614 $message = wp_sprintf(
1615 /* translators: list of active indexables slugs */
1616 esc_html__( 'Indexable not found or inactive. Active indexables are: %l', 'elasticpress' ),
1617 $indexables->get_all( null, true )
1618 );
1619 WP_CLI::error( $message );
1620 }
1621
1622 $this->maybe_add_http_api_debug_filter( $assoc_args );
1623
1624 $object = $indexable->get( $args[1] );
1625 if ( ! $object ) {
1626 WP_CLI::error( esc_html__( 'Not found', 'elasticpress' ) );
1627 }
1628
1629 $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' );
1630
1631 $this->pretty_json_encode( $object, $pretty );
1632 }
1633
1634 /**
1635 * Given associative CLI args, conditionally displays HTTP debug info
1636 *
1637 * @since 4.7.0
1638 * @param array $assoc_args Associative CLI args.
1639 */
1640 protected function maybe_add_http_api_debug_filter( $assoc_args ) {
1641 $debug_http_request = \WP_CLI\Utils\get_flag_value( $assoc_args, 'debug-http-request' );
1642
1643 if ( ! empty( $debug_http_request ) ) {
1644 add_filter(
1645 'http_api_debug',
1646 function ( $response, $context, $transport, $request_args, $url ) {
1647 // phpcs:disable WordPress.PHP.DevelopmentFunctions
1648 WP_CLI::line(
1649 sprintf(
1650 /* translators: URL of the request */
1651 esc_html__( 'URL: %s', 'elasticpress' ),
1652 $url
1653 )
1654 );
1655 WP_CLI::line(
1656 sprintf(
1657 /* translators: Request arguments (outputted with print_r()) */
1658 esc_html__( 'Request Args: %s', 'elasticpress' ),
1659 print_r( $request_args, true )
1660 )
1661 );
1662 WP_CLI::line(
1663 sprintf(
1664 /* translators: HTTP transport used */
1665 esc_html__( 'Transport: %s', 'elasticpress' ),
1666 $transport
1667 )
1668 );
1669 WP_CLI::line(
1670 sprintf(
1671 /* translators: Context under which the http_api_debug hook is fired */
1672 esc_html__( 'Context: %s', 'elasticpress' ),
1673 $context
1674 )
1675 );
1676 WP_CLI::line(
1677 sprintf(
1678 /* translators: HTTP response (outputted with print_r()) */
1679 esc_html__( 'Response: %s', 'elasticpress' ),
1680 print_r( $response, true )
1681 )
1682 );
1683 // phpcs:enable WordPress.PHP.DevelopmentFunctions
1684 },
1685 10,
1686 5
1687 );
1688 }
1689 }
1690 }
1691