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

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

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