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

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

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