PluginProbe
ElasticPress / 4.3.1
ElasticPress v4.3.1
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.3.1, at includes/classes/Command.php

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