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

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

1,584 lines 46.6 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 $indexables = Indexables::factory()->get_all( false );
574
575 foreach ( $indexables as $indexable ) {
576 WP_CLI::line( sprintf( esc_html__( 'Recreating %s network alias…', 'elasticpress' ), esc_html( strtolower( $indexable->labels['singular'] ) ) ) );
577
578 $indexable->delete_network_alias();
579
580 $create_result = $this->create_network_alias_helper( $indexable );
581
582 if ( $create_result ) {
583 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
584 } else {
585 WP_CLI::error( esc_html__( 'An error occurred', 'elasticpress' ) );
586 }
587 }
588 }
589
590 /**
591 * A WP-CLI wrapper to run `Autosuggest::epio_send_autosuggest_public_request()`.
592 *
593 * @param array $args Positional CLI args.
594 * @param array $assoc_args Associative CLI args.
595 * @subcommand epio-set-autosuggest
596 * @since 3.5.x
597 */
598 public function epio_set_autosuggest( $args, $assoc_args ) {
599 $autosuggest_feature = Features::factory()->get_registered_feature( 'autosuggest' );
600
601 if ( empty( $autosuggest_feature ) || ! $autosuggest_feature->is_active() ) {
602 WP_CLI::error( esc_html__( 'Autosuggest is not enabled.', 'elasticpress' ) );
603 }
604
605 add_action( 'ep_epio_wp_cli_set_autosuggest', [ $autosuggest_feature, 'epio_send_autosuggest_public_request' ] );
606
607 do_action( 'ep_epio_wp_cli_set_autosuggest', $args, $assoc_args );
608 }
609
610 /**
611 * Helper method for creating the network alias for an indexable
612 *
613 * @param Indexable $indexable Instance of indexable.
614 * @since 0.9
615 * @return array|bool
616 */
617 private function create_network_alias_helper( Indexable $indexable ) {
618 $sites = Utils\get_sites();
619 $indexes = [];
620
621 foreach ( $sites as $site ) {
622 if ( ! Utils\is_site_indexable( $site['blog_id'] ) ) {
623 continue;
624 }
625
626 switch_to_blog( $site['blog_id'] );
627
628 $indexes[] = $indexable->get_index_name();
629
630 restore_current_blog();
631 }
632
633 return $indexable->create_network_alias( $indexes );
634 }
635
636 /**
637 * Properly clean up when receiving SIGINT on indexing
638 *
639 * @param int $signal_no Signal number
640 * @since 3.3
641 */
642 public function delete_transient_on_int( $signal_no ) {
643 if ( SIGINT === $signal_no ) {
644 $this->delete_transient();
645 WP_CLI::log( esc_html__( 'Indexing cleaned up.', 'elasticpress' ) );
646 exit;
647 }
648 }
649
650 /**
651 * Index all posts for a site or network wide.
652 *
653 * ## OPTIONS
654 *
655 * [--network-wide]
656 * : 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
657 *
658 * [--setup]
659 * : Clear the index first and re-send the put mapping. Use `--yes` to skip the confirmation
660 *
661 * [--per-page=<per_page_number>]
662 * : Determine the amount of posts to be indexed per bulk index (or cycle)
663 *
664 * [--nobulk]
665 * : Disable bulk indexing
666 *
667 * [--static-bulk]
668 * : Do not use dynamic bulk requests, i.e., send only one request per batch of documents.
669 *
670 * [--show-errors]
671 * : Show all errors
672 *
673 * [--show-bulk-errors]
674 * : Display the error message returned from Elasticsearch when a post fails to index using the /_bulk endpoint
675 *
676 * [--show-nobulk-errors]
677 * : Display the error message returned from Elasticsearch when a post fails to index while not using the /_bulk endpoint
678 *
679 * [--offset=<offset_number>]
680 * : Skip the first n posts (don't forget to remove the `--setup` flag when resuming or the index will be emptied before starting again).
681 *
682 * [--indexables=<indexables>]
683 * : Specify the Indexable(s) which will be indexed
684 *
685 * [--post-type=<post_types>]
686 * : 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
687 *
688 * [--include=<IDs>]
689 * : Choose which object IDs to include in the index
690 *
691 * [--post-ids=<IDs>]
692 * : Choose which post_ids to include when indexing the Posts Indexable (deprecated)
693 *
694 * [--upper-limit-object-id=<ID>]
695 * : Upper limit of a range of IDs to be indexed. If indexing IDs from 30 to 45, this should be 45
696 *
697 * [--lower-limit-object-id=<ID>]
698 * : Lower limit of a range of IDs to be indexed. If indexing IDs from 30 to 45, this should be 30
699 *
700 * [--ep-host=<host>]
701 * : Custom Elasticsearch host
702 *
703 * [--ep-prefix=<prefix>]
704 * : Custom ElasticPress prefix
705 *
706 * [--yes]
707 * : Skip confirmation needed by `--setup`
708 *
709 * @param array $args Positional CLI args.
710 * @since 0.1.2
711 * @param array $assoc_args Associative CLI args.
712 */
713 public function index( $args, $assoc_args ) {
714 global $wp_actions;
715
716 $setup_option = isset( $assoc_args['setup'] ) ? $assoc_args['setup'] : false;
717
718 if ( true === $setup_option ) {
719 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 );
720 }
721
722 if ( ! function_exists( 'pcntl_signal' ) ) {
723 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' ) );
724 } else {
725 declare( ticks = 1 );
726 pcntl_signal( SIGINT, [ $this, 'delete_transient_on_int' ] );
727 }
728
729 $this->maybe_change_host( $assoc_args );
730 $this->maybe_change_index_prefix( $assoc_args );
731 $this->connect_check();
732 $this->index_occurring();
733
734 $indexables = null;
735
736 if ( ! empty( $assoc_args['indexables'] ) ) {
737 $indexables = explode( ',', str_replace( ' ', '', $assoc_args['indexables'] ) );
738 }
739
740 /**
741 * Prior to the index command invoking
742 * Useful for deregistering filters/actions that occur during a query request
743 *
744 * @since 1.4.1
745 */
746 /**
747 * Fires before starting a CLI index
748 *
749 * @hook ep_wp_cli_pre_index
750 * @param {array} $args CLI command position args
751 * @param {array} $assoc_args CLI command associative args
752 */
753 do_action( 'ep_wp_cli_pre_index', $args, $assoc_args );
754
755 $this->timer_start();
756
757 add_action( 'ep_sync_put_mapping', [ $this, 'stop_on_failed_mapping' ], 10, 3 );
758 add_action( 'ep_sync_put_mapping', [ $this, 'call_ep_cli_put_mapping' ], 10, 2 );
759 add_action( 'ep_index_batch_new_attempt', [ $this, 'should_interrupt_sync' ] );
760
761 $no_bulk = ! empty( $assoc_args['nobulk'] );
762
763 $index_args = [
764 'method' => 'cli',
765 'total_attempts' => 1,
766 'indexables' => $indexables,
767 'put_mapping' => ! empty( $setup_option ),
768 'output_method' => [ $this, 'index_output' ],
769 'network_wide' => ( ! empty( $assoc_args['network-wide'] ) ) ? $assoc_args['network-wide'] : null,
770 'nobulk' => $no_bulk,
771 'offset' => ( ! empty( $assoc_args['offset'] ) ) ? absint( $assoc_args['offset'] ) : 0,
772 'static_bulk' => ( ! empty( $assoc_args['static-bulk'] ) ) ? $assoc_args['static-bulk'] : null,
773 ];
774
775 if ( isset( $assoc_args['show-errors'] ) || ( isset( $assoc_args['show-bulk-errors'] ) && ! $no_bulk ) || ( isset( $assoc_args['show-nobulk-errors'] ) && $no_bulk ) ) {
776 $index_args['show_errors'] = true;
777 }
778
779 if ( ! empty( $assoc_args['post-ids'] ) ) {
780 $assoc_args['include'] = $assoc_args['post-ids'];
781 }
782
783 if ( ! empty( $assoc_args['include'] ) ) {
784 $include = explode( ',', str_replace( ' ', '', $assoc_args['include'] ) );
785 $index_args['include'] = array_map( 'absint', $include );
786 $index_args['per_page'] = count( $index_args['include'] );
787 }
788
789 if ( ! empty( $assoc_args['per-page'] ) ) {
790 $index_args['per_page'] = absint( $assoc_args['per-page'] );
791 }
792
793 if ( ! empty( $assoc_args['post-type'] ) ) {
794 $index_args['post_type'] = explode( ',', $assoc_args['post-type'] );
795 $index_args['post_type'] = array_map( 'trim', $index_args['post_type'] );
796 // If post-type was passed, only index the Post indexable.
797 $index_args['indexables'] = [ 'post' ];
798 }
799
800 if ( ! empty( $assoc_args['upper-limit-object-id'] ) && is_numeric( $assoc_args['upper-limit-object-id'] ) ) {
801 $index_args['upper_limit_object_id'] = absint( $assoc_args['upper-limit-object-id'] );
802 }
803
804 if ( ! empty( $assoc_args['lower-limit-object-id'] ) && is_numeric( $assoc_args['lower-limit-object-id'] ) ) {
805 $index_args['lower_limit_object_id'] = absint( $assoc_args['lower-limit-object-id'] );
806 }
807
808 \ElasticPress\IndexHelper::factory()->full_index( $index_args );
809
810 remove_action( 'ep_sync_put_mapping', [ $this, 'stop_on_failed_mapping' ] );
811 remove_action( 'ep_sync_put_mapping', [ $this, 'call_ep_cli_put_mapping' ], 10, 2 );
812 remove_action( 'ep_index_batch_new_attempt', [ $this, 'should_interrupt_sync' ] );
813
814 $sync_time_in_ms = $this->timer_stop();
815
816 /**
817 * Fires after executing a CLI index
818 *
819 * @hook ep_wp_cli_after_index
820 * @param {array} $args CLI command position args
821 * @param {array} $assoc_args CLI command associative args
822 *
823 * @since 3.5.5
824 */
825 do_action( 'ep_wp_cli_after_index', $args, $assoc_args );
826
827 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Total time elapsed: ', 'elasticpress' ) . '%N' . $this->timer_format( $sync_time_in_ms ) ) );
828
829 $this->delete_transient();
830
831 WP_CLI::success( esc_html__( 'Done!', 'elasticpress' ) );
832 }
833
834 /**
835 * Ping the Elasticsearch server and retrieve a status.
836 *
837 * @since 0.9.1
838 */
839 public function status() {
840 $this->connect_check();
841
842 $request_args = [ 'headers' => Elasticsearch::factory()->format_request_headers() ];
843
844 $registered_index_names = $this->get_index_names();
845
846 $response_cat_indices = Elasticsearch::factory()->remote_request( '_cat/indices?format=json' );
847
848 if ( is_wp_error( $response_cat_indices ) ) {
849 WP_CLI::error( implode( "\n", $response_cat_indices->get_error_messages() ) );
850 }
851
852 $indexes_from_cat_indices_api = json_decode( wp_remote_retrieve_body( $response_cat_indices ), true );
853
854 if ( is_array( $indexes_from_cat_indices_api ) ) {
855 $indexes_from_cat_indices_api = wp_list_pluck( $indexes_from_cat_indices_api, 'index' );
856
857 $index_names = array_intersect( $registered_index_names, $indexes_from_cat_indices_api );
858 } else {
859 WP_CLI::error( esc_html__( 'Failed to return status.', 'elasticpress' ) );
860 }
861
862 $index_names_imploded = implode( ',', $index_names );
863
864 $request = wp_remote_get( trailingslashit( Utils\get_host( true ) ) . $index_names_imploded . '/_recovery/?pretty', $request_args );
865
866 if ( is_wp_error( $request ) ) {
867 WP_CLI::error( implode( "\n", $request->get_error_messages() ) );
868 }
869
870 $body = wp_remote_retrieve_body( $request );
871 WP_CLI::line( '' );
872 WP_CLI::line( '====== Status ======' );
873 // phpcs:disable
874 WP_CLI::line( print_r( $body, true ) );
875 // phpcs:enable
876 WP_CLI::line( '====== End Status ======' );
877 }
878
879 /**
880 * Get stats on the current index.
881 *
882 * @since 0.9.2
883 */
884 public function stats() {
885 $this->connect_check();
886
887 $request_args = array( 'headers' => Elasticsearch::factory()->format_request_headers() );
888
889 $registered_index_names = $this->get_index_names();
890
891 $response_cat_indices = Elasticsearch::factory()->remote_request( '_cat/indices?format=json' );
892
893 if ( is_wp_error( $response_cat_indices ) ) {
894 WP_CLI::error( implode( "\n", $response_cat_indices->get_error_messages() ) );
895 }
896
897 $indexes_from_cat_indices_api = json_decode( wp_remote_retrieve_body( $response_cat_indices ), true );
898
899 if ( is_array( $indexes_from_cat_indices_api ) ) {
900 $indexes_from_cat_indices_api = wp_list_pluck( $indexes_from_cat_indices_api, 'index' );
901
902 $index_names = array_intersect( $registered_index_names, $indexes_from_cat_indices_api );
903 } else {
904 WP_CLI::error( esc_html__( 'Failed to return stats.', 'elasticpress' ) );
905 }
906
907 $index_names_imploded = implode( ',', $index_names );
908
909 $request = wp_remote_get( trailingslashit( Utils\get_host( true ) ) . $index_names_imploded . '/_stats/', $request_args );
910
911 if ( is_wp_error( $request ) ) {
912 WP_CLI::error( implode( "\n", $request->get_error_messages() ) );
913 }
914 $body = json_decode( wp_remote_retrieve_body( $request ), true );
915
916 foreach ( $registered_index_names as $index_name ) {
917 $this->render_stats( $index_name, $body );
918 }
919 }
920
921 /**
922 * Provide better error messaging for common connection errors
923 *
924 * @since 0.9.3
925 */
926 private function connect_check() {
927 $host = Utils\get_host();
928
929 if ( empty( $host ) ) {
930 WP_CLI::error( esc_html__( 'Elasticsearch host is not set.', 'elasticpress' ) );
931 } elseif ( ! Elasticsearch::factory()->get_elasticsearch_version( true ) ) {
932 WP_CLI::error( esc_html__( 'Could not connect to Elasticsearch.', 'elasticpress' ) );
933 }
934 }
935
936 /**
937 * Error out if index is already occurring
938 *
939 * @since 3.0
940 */
941 private function index_occurring() {
942 if ( Utils\is_indexing() ) {
943 WP_CLI::error( esc_html__( 'An index is already occurring. Try again later.', 'elasticpress' ) );
944 }
945 }
946
947 /**
948 * Delete transient that indicates indexing is occurring
949 *
950 * @since 3.1
951 */
952 private function delete_transient() {
953 \ElasticPress\IndexHelper::factory()->clear_index_meta();
954
955 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
956 delete_site_transient( 'ep_cli_sync_progress' );
957 delete_site_transient( 'ep_wpcli_sync_interrupted' );
958 } else {
959 delete_transient( 'ep_cli_sync_progress' );
960 delete_transient( 'ep_wpcli_sync_interrupted' );
961 }
962 }
963
964 /**
965 * Clear a sync/index process.
966 *
967 * If an index was stopped prematurely and won't start again, this will clear this cached data such that a new index can start.
968 *
969 * @subcommand clear-index
970 * @alias delete-transient
971 * @since 3.4
972 */
973 public function clear_index() {
974 /**
975 * Fires before the CLI `clear-index` command is executed.
976 *
977 * @hook ep_cli_before_clear_index
978 *
979 * @since 3.5.5
980 */
981 do_action( 'ep_cli_before_clear_index' );
982
983 $this->delete_transient();
984
985 /**
986 * Fires after the CLI `clear-index` command is executed.
987 *
988 * @hook ep_cli_after_clear_index
989 *
990 * @since 3.5.5
991 */
992 do_action( 'ep_cli_after_clear_index' );
993
994 WP_CLI::log( esc_html__( 'Index cleared.', 'elasticpress' ) );
995 }
996
997 /**
998 * Returns the status of an ongoing index operation in JSON array.
999 *
1000 * Returns the status of an ongoing index operation in JSON array with the following fields:
1001 * indexing | boolean | True if index operation is ongoing or false
1002 * method | string | 'cli', 'web' or 'none'
1003 * items_indexed | integer | Total number of items indexed
1004 * total_items | integer | Total number of items indexed or -1 if not yet determined
1005 *
1006 * ## OPTIONS
1007 *
1008 * [--pretty]
1009 * : Use this flag to render a pretty-printed version of the JSON response.
1010 *
1011 * @subcommand get-indexing-status
1012 * @since 3.5.1, `--pretty` introduced in 4.1.0
1013 * @param array $args Positional CLI args.
1014 * @param array $assoc_args Associative CLI args.
1015 */
1016 public function get_indexing_status( $args, $assoc_args ) {
1017 $indexing_status = Utils\get_indexing_status();
1018
1019 if ( empty( $indexing_status ) ) {
1020 $indexing_status = [
1021 'indexing' => false,
1022 'method' => 'none',
1023 'items_indexed' => 0,
1024 'total_items' => -1,
1025 ];
1026 }
1027
1028 $this->pretty_json_encode( $indexing_status, ! empty( $assoc_args['pretty'] ) );
1029 }
1030
1031 /**
1032 * Returns a JSON array with the results of the last index (if present) or an empty array.
1033 *
1034 * ## OPTIONS
1035 *
1036 * [--pretty]
1037 * : Use this flag to render a pretty-printed version of the JSON response.
1038 *
1039 * @subcommand get-last-sync
1040 * @alias get-last-index
1041 * @since 4.2.0
1042 * @param array $args Positional CLI args.
1043 * @param array $assoc_args Associative CLI args.
1044 */
1045 public function get_last_sync( $args, $assoc_args ) {
1046 $last_sync = \ElasticPress\IndexHelper::factory()->get_last_index();
1047
1048 $this->pretty_json_encode( $last_sync, ! empty( $assoc_args['pretty'] ) );
1049 }
1050
1051 /**
1052 * Returns a JSON array with the results of the last CLI index (if present) or an empty array.
1053 *
1054 * ## OPTIONS
1055 *
1056 * [--clear]
1057 * : Clear the `ep_last_cli_index` option.
1058 *
1059 * [--pretty]
1060 * : Use this flag to render a pretty-printed version of the JSON response.
1061 *
1062 * @subcommand get-last-cli-index
1063 * @since 3.5.1, `--pretty` introduced in 4.1.0
1064 * @param array $args Positional CLI args.
1065 * @param array $assoc_args Associative CLI args.
1066 */
1067 public function get_last_cli_index( $args, $assoc_args ) {
1068 $last_sync = Utils\get_option( 'ep_last_cli_index', array() );
1069
1070 if ( isset( $assoc_args['clear'] ) ) {
1071 Utils\delete_option( 'ep_last_cli_index' );
1072 }
1073
1074 $this->pretty_json_encode( $last_sync, ! empty( $assoc_args['pretty'] ) );
1075 }
1076
1077
1078 /**
1079 * maybe change Elastic host on the fly
1080 *
1081 * @param array $assoc_args Associative CLI args.
1082 *
1083 * @since 3.4
1084 */
1085 private function maybe_change_host( $assoc_args ) {
1086 if ( isset( $assoc_args['ep-host'] ) ) {
1087 add_filter(
1088 'ep_host',
1089 function ( $host ) use ( $assoc_args ) {
1090 return $assoc_args['ep-host'];
1091 }
1092 );
1093 }
1094 }
1095
1096
1097 /**
1098 * maybe change index prefix on the fly
1099 *
1100 * @param array $assoc_args Associative CLI args.
1101 *
1102 * @since 3.4
1103 */
1104 private function maybe_change_index_prefix( $assoc_args ) {
1105 if ( isset( $assoc_args['ep-prefix'] ) ) {
1106 add_filter(
1107 'ep_index_prefix',
1108 function ( $prefix ) use ( $assoc_args ) {
1109 return $assoc_args['ep-prefix'];
1110 }
1111 );
1112 }
1113 }
1114
1115 /**
1116 * Check if sync should be interrupted
1117 *
1118 * @since 3.5.2
1119 */
1120 public function should_interrupt_sync() {
1121 $should_interrupt_sync = get_transient( 'ep_wpcli_sync_interrupted' );
1122
1123 if ( $should_interrupt_sync ) {
1124 WP_CLI::line( esc_html__( 'Sync was interrupted', 'elasticpress' ) );
1125 $this->delete_transient_on_int( 2 );
1126 WP_CLI::halt();
1127 }
1128 }
1129
1130 /**
1131 * Stop the indexing operation started from the dashboard.
1132 *
1133 * @subcommand stop-indexing
1134 * @since 3.5.2
1135 * @param array $args Positional CLI args.
1136 * @param array $assoc_args Associative CLI args.
1137 */
1138 public function stop_indexing( $args, $assoc_args ) {
1139 $indexing_status = \ElasticPress\Utils\get_indexing_status();
1140
1141 if ( empty( \ElasticPress\Utils\get_indexing_status() ) ) {
1142 WP_CLI::warning( esc_html__( 'There is no indexing operation running.', 'elasticpress' ) );
1143 } else {
1144 WP_CLI::line( esc_html__( 'Stopping indexing…', 'elasticpress' ) );
1145
1146 if ( isset( $indexing_status['method'] ) && 'cli' === $indexing_status['method'] ) {
1147 set_transient( 'ep_wpcli_sync_interrupted', true, MINUTE_IN_SECONDS );
1148 } else {
1149 set_transient( 'ep_sync_interrupted', true, MINUTE_IN_SECONDS );
1150 }
1151
1152 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1153 }
1154 }
1155
1156 /**
1157 * Set the algorithm version.
1158 *
1159 * Set the algorithm version through the `ep_search_algorithm_version` option,
1160 * that will be used by the filter with same name.
1161 * Delete the option if `--default` is passed.
1162 *
1163 * ## OPTIONS
1164 *
1165 * [--version=<version>]
1166 * : Version name
1167 *
1168 * [--default]
1169 * : Use to set the default version
1170 *
1171 * @subcommand set-algorithm-version
1172 *
1173 * @since 3.5.4
1174 * @param array $args Positional CLI args.
1175 * @param array $assoc_args Associative CLI args.
1176 */
1177 public function set_search_algorithm_version( $args, $assoc_args ) {
1178 /**
1179 * Fires before the algorithm version is changed via WP-CLI.
1180 *
1181 * @hook ep_cli_before_set_search_algorithm_version
1182 * @param {array} $args CLI command position args
1183 * @param {array} $assoc_args CLI command associative args
1184 *
1185 * @since 3.5.5
1186 */
1187 do_action( 'ep_cli_before_set_search_algorithm_version', $args, $assoc_args );
1188
1189 if ( empty( $assoc_args['version'] ) && ! isset( $assoc_args['default'] ) ) {
1190 WP_CLI::error( esc_html__( 'This command expects a version number or the --default flag.', 'elasticpress' ) );
1191 }
1192
1193 if ( ! empty( $assoc_args['default'] ) ) {
1194 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1195 delete_site_option( 'ep_search_algorithm_version' );
1196 } else {
1197 delete_option( 'ep_search_algorithm_version' );
1198 }
1199 } else {
1200 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1201 update_site_option( 'ep_search_algorithm_version', $assoc_args['version'] );
1202 } else {
1203 update_option( 'ep_search_algorithm_version', $assoc_args['version'], false );
1204 }
1205 }
1206
1207 /**
1208 * Fires after the algorithm version is changed via WP-CLI.
1209 *
1210 * @hook ep_cli_after_set_search_algorithm_version
1211 * @param {array} $args CLI command position args
1212 * @param {array} $assoc_args CLI command associative args
1213 *
1214 * @since 3.5.5
1215 */
1216 do_action( 'ep_cli_after_set_search_algorithm_version', $args, $assoc_args );
1217
1218 WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) );
1219 }
1220
1221 /**
1222 * Get the algorithm version.
1223 *
1224 * Get the value of the `ep_search_algorithm_version` option, or
1225 * `default` if empty.
1226 *
1227 * @subcommand get-algorithm-version
1228 *
1229 * @since 3.5.4
1230 * @param array $args Positional CLI args.
1231 * @param array $assoc_args Associative CLI args.
1232 */
1233 public function get_search_algorithm_version( $args, $assoc_args ) {
1234 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1235 $value = get_site_option( 'ep_search_algorithm_version', '' );
1236 } else {
1237 $value = get_option( 'ep_search_algorithm_version', '' );
1238 }
1239
1240 if ( empty( $value ) ) {
1241 WP_CLI::line( 'default' );
1242 } else {
1243 WP_CLI::line( $value );
1244 }
1245 }
1246
1247 /**
1248 * Custom get_transient to WP-CLI env.
1249 *
1250 * We are using the direct SQL query instead of
1251 * the regular function call to retrieve the updated
1252 * value to stop the sync. Otherwise, we always get
1253 * false after the command is running even when the value
1254 * is updated.
1255 *
1256 * @since 3.5.2
1257 * @param mixed $pre_transient The default value.
1258 * @param string $transient Transient name.
1259 * @return true|null
1260 */
1261 public function custom_get_transient( $pre_transient, $transient ) {
1262 global $wpdb;
1263
1264 if ( wp_using_ext_object_cache() ) {
1265 /**
1266 * When external object cache is used we need to make sure to force a remote fetch,
1267 * so that the value from the local memory is discarded.
1268 */
1269 $should_interrupt_sync = wp_cache_get( $transient, 'transient', true );
1270 } else {
1271 $options = $wpdb->options;
1272
1273 $should_interrupt_sync = $wpdb->get_var(
1274 // phpcs:disable
1275 $wpdb->prepare(
1276 "
1277 SELECT option_value
1278 FROM $options
1279 WHERE option_name = %s
1280 LIMIT 1
1281 ",
1282 "_transient_{$transient}"
1283 )
1284 // phpcs:enable
1285 );
1286 }
1287
1288 return $should_interrupt_sync ? (bool) $should_interrupt_sync : null;
1289 }
1290
1291 /**
1292 * Utilitary function to render Stats for a given index.
1293 *
1294 * @since 3.5.6
1295 * @param string $current_index The index name.
1296 * @param array $body The response body.
1297 * @return void
1298 */
1299 protected function render_stats( $current_index, $body ) {
1300 if ( isset( $body['indices'][ $current_index ] ) ) {
1301 WP_CLI::log( '====== Stats for: ' . $current_index . ' ======' );
1302 WP_CLI::log( 'Documents: ' . $body['indices'][ $current_index ]['primaries']['docs']['count'] );
1303 WP_CLI::log( 'Index Size: ' . size_format( $body['indices'][ $current_index ]['primaries']['store']['size_in_bytes'], 2 ) );
1304 WP_CLI::log( 'Index Size (including replicas): ' . size_format( $body['indices'][ $current_index ]['total']['store']['size_in_bytes'], 2 ) );
1305 WP_CLI::log( '====== End Stats ======' );
1306 } else {
1307 WP_CLI::warning( $current_index . ' is not currently indexed.' );
1308 }
1309 }
1310
1311 /**
1312 * Function used to ouput messages coming from IndexHelper
1313 *
1314 * @param array $message Message data
1315 * @param array $args Args sent and processed by IndexHelper
1316 * @param array $index_meta Current index state
1317 * @param string $context Context of the message being outputted
1318 */
1319 public function index_output( $message, $args, $index_meta, $context ) {
1320 static $time_elapsed = 0, $counter = 0;
1321
1322 switch ( $message['status'] ) {
1323 case 'success':
1324 WP_CLI::success( $message['message'] );
1325 break;
1326
1327 case 'warning':
1328 if ( empty( $args['show_errors'] ) ) {
1329 return;
1330 }
1331 WP_CLI::warning( $message['message'] );
1332 break;
1333
1334 case 'error':
1335 $this->clear_index();
1336 WP_CLI::error( $message['message'] );
1337 break;
1338
1339 default:
1340 WP_CLI::log( $message['message'] );
1341 break;
1342 }
1343
1344 if ( 'index_next_batch' === $context ) {
1345 $counter++;
1346 if ( ( $counter % 10 ) === 0 ) {
1347 $time_elapsed_diff = $time_elapsed > 0 ? ' (+' . (string) ( $this->timer_stop() - $time_elapsed ) . ')' : '';
1348 $time_elapsed = $this->timer_stop( 2 );
1349 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Time elapsed: ', 'elasticpress' ) . '%N' . $this->timer_format( $time_elapsed ) . $time_elapsed_diff ) );
1350
1351 $current_memory = round( memory_get_usage() / 1024 / 1024, 2 ) . 'mb';
1352 $peak_memory = ' (Peak: ' . round( memory_get_peak_usage() / 1024 / 1024, 2 ) . 'mb)';
1353 WP_CLI::log( WP_CLI::colorize( '%Y' . esc_html__( 'Memory Usage: ', 'elasticpress' ) . '%N' . $current_memory . $peak_memory ) );
1354 }
1355 }
1356 }
1357
1358 /**
1359 * If put_mapping fails while indexing, stop the index process.
1360 *
1361 * @param array $index_meta Index meta info
1362 * @param Indexable $indexable Indexable object
1363 * @param bool $result Whether the request was successful or not
1364 */
1365 public function stop_on_failed_mapping( $index_meta, $indexable, $result ) {
1366 if ( ! $result ) {
1367 $this->delete_transient();
1368
1369 exit( 1 );
1370 }
1371 }
1372
1373 /**
1374 * Ties the `ep_cli_put_mapping` action to `ep_sync_put_mapping`.
1375 *
1376 * @since 4.0.0
1377 *
1378 * @param array $index_meta Index meta information
1379 * @param Indexable $indexable Indexable object
1380 * @return void
1381 */
1382 public function call_ep_cli_put_mapping( $index_meta, $indexable ) {
1383 /**
1384 * Fires after CLI put mapping
1385 *
1386 * @hook ep_cli_put_mapping
1387 * @param {Indexable} $indexable Indexable involved in mapping
1388 * @param {array} $args CLI command position args
1389 * @param {array} $assoc_args CLI command associative args
1390 */
1391 do_action( 'ep_cli_put_mapping', $indexable, $this->args, $this->assoc_args );
1392 }
1393
1394 /**
1395 * Send a HTTP request to Elasticsearch
1396 *
1397 * ## OPTIONS
1398 *
1399 * <path>
1400 * : Path of the request. Example: `_cat/indices`
1401 *
1402 * [--method=<method>]
1403 * : HTTP Method (GET, POST, etc.)
1404 *
1405 * [--body=<json-body>]
1406 * : Request body
1407 *
1408 * [--debug-http-request]
1409 * : Enable debugging
1410 *
1411 * [--pretty]
1412 * : Use this flag to render a pretty-printed version of the JSON response.
1413 *
1414 * @subcommand request
1415 *
1416 * @since 3.6.6, `--pretty` introduced in 4.1.0
1417 *
1418 * @param array $args Positional CLI args.
1419 * @param array $assoc_args Associative CLI args.
1420 */
1421 public function request( $args, $assoc_args ) {
1422 $path = $args[0];
1423 $method = isset( $assoc_args['method'] ) ? $assoc_args['method'] : 'GET';
1424 $body = isset( $assoc_args['body'] ) ? $assoc_args['body'] : '';
1425 $request_args = [
1426 'method' => $method,
1427 ];
1428 if ( 'GET' !== $method && ! empty( $body ) ) {
1429 $request_args['body'] = $body;
1430 }
1431
1432 if ( ! empty( $assoc_args['debug-http-request'] ) ) {
1433 add_filter(
1434 'http_api_debug',
1435 function ( $response, $context, $transport, $request_args, $url ) {
1436 // phpcs:disable WordPress.PHP.DevelopmentFunctions
1437 WP_CLI::line(
1438 sprintf(
1439 /* translators: URL of the request */
1440 esc_html__( 'URL: %s', 'elasticpress' ),
1441 $url
1442 )
1443 );
1444 WP_CLI::line(
1445 sprintf(
1446 /* translators: Request arguments (outputted with print_r()) */
1447 esc_html__( 'Request Args: %s', 'elasticpress' ),
1448 print_r( $request_args, true )
1449 )
1450 );
1451 WP_CLI::line(
1452 sprintf(
1453 /* translators: HTTP transport used */
1454 esc_html__( 'Transport: %s', 'elasticpress' ),
1455 $transport
1456 )
1457 );
1458 WP_CLI::line(
1459 sprintf(
1460 /* translators: Context under which the http_api_debug hook is fired */
1461 esc_html__( 'Context: %s', 'elasticpress' ),
1462 $context
1463 )
1464 );
1465 WP_CLI::line(
1466 sprintf(
1467 /* translators: HTTP response (outputted with print_r()) */
1468 esc_html__( 'Response: %s', 'elasticpress' ),
1469 print_r( $response, true )
1470 )
1471 );
1472 // phpcs:enable WordPress.PHP.DevelopmentFunctions
1473 },
1474 10,
1475 5
1476 );
1477 }
1478 $response = Elasticsearch::factory()->remote_request( $path, $request_args, [], 'wp_cli_request' );
1479
1480 if ( is_wp_error( $response ) ) {
1481 WP_CLI::error( $response->get_error_message() );
1482 }
1483
1484 $this->print_json_response( $response, ! empty( $assoc_args['pretty'] ) );
1485 }
1486
1487 /**
1488 * Reset all ElasticPress settings stored in WP options and transients.
1489 *
1490 * This command will not delete any index or content stored in Elasticsearch but will force users to go through the installation process again.
1491 *
1492 * ## OPTIONS
1493 *
1494 * [--yes]
1495 * : Skip confirmation
1496 *
1497 * @subcommand settings-reset
1498 *
1499 * @since 4.2.0
1500 *
1501 * @param array $args Positional CLI args.
1502 * @param array $assoc_args Associative CLI args.
1503 */
1504 public function settings_reset( $args, $assoc_args ) {
1505 WP_CLI::confirm( esc_html__( 'Are you sure you want to delete all ElasticPress settings?', 'elasticpress' ), $assoc_args );
1506
1507 define( 'EP_MANUAL_SETTINGS_RESET', true );
1508 include EP_PATH . '/uninstall.php';
1509
1510 WP_CLI::line( esc_html__( 'Settings deleted.', 'elasticpress' ) );
1511 }
1512
1513 /**
1514 * Starts the timer.
1515 *
1516 * @since 4.2.0
1517 * @return true
1518 */
1519 protected function timer_start() {
1520 $this->time_start = microtime( true );
1521 return true;
1522 }
1523
1524 /**
1525 * Stops the timer.
1526 *
1527 * @since 4.2.0
1528 * @param int $precision The number of digits from the right of the decimal to display. Default 3.
1529 * @return float Time spent so far
1530 */
1531 protected function timer_stop( $precision = 3 ) {
1532 $diff = microtime( true ) - $this->time_start;
1533 return (float) number_format( (float) $diff, $precision );
1534 }
1535
1536 /**
1537 * Given a timestamp in microseconds, returns it in the given format.
1538 *
1539 * @since 4.2.0
1540 * @param float $microtime Unix timestamp in ms
1541 * @param string $format Desired format
1542 * @return string
1543 */
1544 protected function timer_format( $microtime, $format = 'H:i:s.u' ) {
1545 $microtime_date = \DateTime::createFromFormat( 'U.u', number_format( (float) $microtime, 3, '.', '' ) );
1546 return $microtime_date->format( $format );
1547 }
1548
1549 /**
1550 * Print an HTTP response.
1551 *
1552 * @since 4.1.0
1553 * @param array $response HTTP Response.
1554 * @param boolean $pretty Whether the JSON response should be formatted or not.
1555 */
1556 protected function print_json_response( $response, $pretty ) {
1557 $response_body = wp_remote_retrieve_body( $response );
1558
1559 $content_type = wp_remote_retrieve_header( $response, 'Content-Type' );
1560
1561 if ( ! $pretty || ! preg_match( '/json/', $content_type ) ) {
1562 WP_CLI::line( $response_body );
1563 return;
1564 }
1565
1566 // Re-encode the JSON to add space formatting
1567 $response_body_obj = json_decode( $response_body );
1568
1569 $this->pretty_json_encode( $response_body_obj, JSON_PRETTY_PRINT );
1570 }
1571
1572 /**
1573 * Output a JSON object. Conditionally format it before doing so.
1574 *
1575 * @since 4.1.0
1576 * @param array $json_obj The JSON object or array.
1577 * @param boolean $pretty_print_flag Whether it should or not be formatted.
1578 */
1579 protected function pretty_json_encode( $json_obj, $pretty_print_flag ) {
1580 $flag = $pretty_print_flag ? JSON_PRETTY_PRINT : null;
1581 WP_CLI::line( wp_json_encode( $json_obj, $flag ) );
1582 }
1583 }
1584