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

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

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