PluginProbe
ElasticPress / 4.5.1
ElasticPress v4.5.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.5.1, at includes/classes/Command.php

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