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

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

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