PluginProbe
ElasticPress / 4.4.1
ElasticPress v4.4.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.4.1, at includes/classes/Command.php

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