PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.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 / IndexHelper.php

IndexHelper.php in ElasticPress 5.0.0, at includes/classes/IndexHelper.php

1,556 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 * Index Helper
4 *
5 * NOTE: As explained in the doc linked below, the dashboard sync exits after each output()
6 * call, to respond to the AJAX request. That means this script will be called several times
7 * while syncing via dashboard, relying on the index_meta to pick it up where it stopped.
8 *
9 * @since 4.0.0
10 * @see https://elasticpress.zendesk.com/hc/en-us/articles/16672117103501-Sync-Process
11 * @package elasticpress
12 */
13
14 namespace ElasticPress;
15
16 use ElasticPress\Utils;
17
18 /**
19 * Index Helper Class.
20 *
21 * @since 4.0.0
22 */
23 class IndexHelper {
24 /**
25 * Array to hold all the index sync information.
26 *
27 * @since 4.0.0
28 * @var array|bool
29 */
30 protected $index_meta = false;
31
32 /**
33 * Arguments to be used during the index process.
34 *
35 * @var array
36 */
37 protected $args = [];
38
39 /**
40 * Queried objects of the current sync item in the stack.
41 *
42 * @since 4.0.0
43 * @var array
44 */
45 protected $current_query = [];
46
47 /**
48 * Holds temporary wp_actions when indexing with pagination
49 *
50 * @since 4.0.0
51 * @var array
52 */
53 private $temporary_wp_actions = [];
54
55 /**
56 * Initialize class.
57 *
58 * @since 4.0.0
59 */
60 public function setup() {
61 $this->index_meta = Utils\get_indexing_status();
62 }
63
64 /**
65 * Method to index everything.
66 *
67 * @since 4.0.0
68 * @param array $args Arguments.
69 */
70 public function full_index( $args ) {
71 register_shutdown_function( [ $this, 'handle_index_error' ] );
72 add_filter( 'wp_php_error_message', [ $this, 'wp_handle_index_error' ], 10, 2 );
73
74 $this->index_meta = Utils\get_indexing_status();
75
76 /**
77 * Filter the sync arguments
78 *
79 * @since 4.5.0
80 * @hook ep_sync_args
81 * @param {array} $args Sync arguments
82 * @param {array} $index_meta Current index meta
83 * @return {array} New sync arguments
84 */
85 $this->args = apply_filters( 'ep_sync_args', $args, $this->index_meta );
86
87 if ( false === $this->index_meta ) {
88 $this->maybe_apply_feature_settings();
89 $this->build_index_meta();
90 }
91
92 // For the dashboard, this will be called and exit the script until the queue is empty again.
93 $this->flush_messages_queue();
94
95 while ( $this->has_items_to_be_processed() ) {
96 $this->process_sync_item();
97 }
98
99 while ( $this->has_network_alias_to_be_created() ) {
100 $this->create_network_alias();
101 }
102
103 $this->full_index_complete();
104 }
105
106 /**
107 * Method to stack everything that needs to be indexed.
108 *
109 * @since 4.0.0
110 */
111 protected function build_index_meta() {
112 Utils\update_option( 'ep_last_sync', time() );
113 Utils\delete_option( 'ep_need_upgrade_sync' );
114 Utils\delete_option( 'ep_feature_auto_activated_sync' );
115 delete_transient( 'ep_sync_interrupted' );
116
117 $start_date_time = date_create( 'now', wp_timezone() );
118
119 /**
120 * There are two ways to control pagination of things that need to be indexed:
121 * - offset: The number of items to skip on each iteration
122 * - id range: Given an ID range, process a batch and set the upper limit as the last processed ID -1
123 *
124 * Although in the first case offset is updated to really control the flow, in the
125 * second it is updated to simply output the number of items processed.
126 */
127 $pagination_method = ( ! empty( $this->args['offset'] ) || ! empty( $this->args['post-ids'] ) || ! empty( $this->args['include'] ) ) ?
128 'offset' :
129 'id_range';
130
131 $starting_indices = array_intersect(
132 Elasticsearch::factory()->get_index_names( 'all' ),
133 wp_list_pluck( Elasticsearch::factory()->get_cluster_indices(), 'index' )
134 );
135
136 $this->index_meta = [
137 'method' => ! empty( $this->args['method'] ) ? $this->args['method'] : 'web',
138 'put_mapping' => ! empty( $this->args['put_mapping'] ),
139 'offset' => ! empty( $this->args['offset'] ) ? absint( $this->args['offset'] ) : 0,
140 'pagination_method' => $pagination_method,
141 'start' => true,
142 'sync_stack' => [],
143 'network_alias' => [],
144 'start_time' => microtime( true ),
145 'start_date_time' => $start_date_time ? $start_date_time->format( DATE_ATOM ) : false,
146 'starting_indices' => $starting_indices,
147 'messages_queue' => [],
148 'trigger' => ! empty( $this->args['trigger'] ) ? sanitize_text_field( $this->args['trigger'] ) : null,
149 'totals' => [
150 'total' => 0,
151 'synced' => 0,
152 'skipped' => 0,
153 'failed' => 0,
154 'total_time' => 0,
155 'errors' => [],
156 ],
157 ];
158
159 $global_indexables = $this->filter_indexables( Indexables::factory()->get_all( true, true, 'all' ) );
160 $non_global_indexables = $this->filter_indexables( Indexables::factory()->get_all( false, true, 'all' ) );
161
162 $is_network_wide = isset( $this->args['network_wide'] ) && ! is_null( $this->args['network_wide'] );
163
164 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && $is_network_wide ) {
165 if ( ! is_numeric( $this->args['network_wide'] ) ) {
166 $this->args['network_wide'] = 0;
167 }
168
169 $sites = Utils\get_sites( $this->args['network_wide'], true );
170
171 foreach ( $sites as $site ) {
172 switch_to_blog( $site['blog_id'] );
173
174 foreach ( $non_global_indexables as $indexable ) {
175 $this->add_sync_item_to_stack(
176 [
177 'url' => untrailingslashit( $site['domain'] . $site['path'] ),
178 'blog_id' => (int) $site['blog_id'],
179 'indexable' => $indexable,
180 ]
181 );
182
183 if ( Indexables::factory()->is_active( $indexable ) && ! in_array( $indexable, $this->index_meta['network_alias'], true ) ) {
184 $this->index_meta['network_alias'][] = $indexable;
185 }
186 }
187 }
188
189 restore_current_blog();
190 } else {
191 foreach ( $non_global_indexables as $indexable ) {
192 $this->add_sync_item_to_stack(
193 [
194 'url' => untrailingslashit( home_url() ),
195 'blog_id' => (int) get_current_blog_id(),
196 'indexable' => $indexable,
197 ]
198 );
199 }
200 }
201
202 foreach ( $global_indexables as $indexable ) {
203 $this->add_sync_item_to_stack(
204 [
205 'indexable' => $indexable,
206 ]
207 );
208 }
209
210 $this->index_meta['current_sync_item'] = false;
211 /**
212 * Fires at start of new index
213 *
214 * @since 4.0.0
215 *
216 * @hook ep_sync_start_index
217 * @param {array} $index_meta Index meta information
218 */
219 do_action( 'ep_sync_start_index', $this->index_meta );
220
221 /**
222 * Fires at start of new index
223 *
224 * @since 2.1 Previously called only as 'ep_dashboard_start_index'
225 * @since 4.0.0 Made available for all methods
226 *
227 * @hook ep_{$index_method}_start_index
228 * @param {array} $index_meta Index meta information
229 */
230 do_action( "ep_{$this->args['method']}_start_index", $this->index_meta );
231
232 /**
233 * Filter index meta during dashboard sync
234 *
235 * @since 3.0
236 * @hook ep_index_meta
237 * @param {array} $index_meta Current index meta
238 * @return {array} New index meta
239 */
240 $this->index_meta = apply_filters( 'ep_index_meta', $this->index_meta );
241 }
242
243 /**
244 * Given an array of indexables, check if they are part of the indexable args or not.
245 *
246 * @since 4.0.0
247 * @param array $indexables Indexable slugs.
248 * @return array
249 */
250 protected function filter_indexables( $indexables ) {
251 return array_filter(
252 $indexables,
253 function( $indexable ) {
254 return empty( $this->args['indexables'] ) || in_array( $indexable, $this->args['indexables'], true );
255 }
256 );
257 }
258
259 /**
260 * Check if there are still items to be processed in the stack.
261 *
262 * @since 4.0.0
263 * @return boolean
264 */
265 protected function has_items_to_be_processed() {
266 return ! empty( $this->index_meta['current_sync_item'] ) || count( $this->index_meta['sync_stack'] ) > 0;
267 }
268
269 /**
270 * Method to process the next item in the stack.
271 *
272 * @since 4.0.0
273 */
274 protected function process_sync_item() {
275 if ( empty( $this->index_meta['current_sync_item'] ) ) {
276 $this->index_meta['current_sync_item'] = array_merge(
277 array_shift( $this->index_meta['sync_stack'] ),
278 [
279 'total' => 0,
280 'synced' => 0,
281 'skipped' => 0,
282 'failed' => 0,
283 'errors' => [],
284 ]
285 );
286
287 $indexable_slug = $this->index_meta['current_sync_item']['indexable'];
288 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
289
290 if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
291 return $this->process_not_active_indexable_sync_item();
292 } elseif ( ! empty( $this->index_meta['current_sync_item']['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
293 $this->output_success(
294 sprintf(
295 /* translators: 1: Indexable name, 2: Site ID */
296 esc_html__( 'Indexing %1$s on site %2$d…', 'elasticpress' ),
297 esc_html( strtolower( $indexable->labels['plural'] ) ),
298 $this->index_meta['current_sync_item']['blog_id']
299 )
300 );
301 } else {
302 $message_string = ( $indexable->global ) ?
303 /* translators: 1: Indexable name */
304 esc_html__( 'Indexing %1$s (globally)…', 'elasticpress' ) :
305 /* translators: 1: Indexable name */
306 esc_html__( 'Indexing %1$s…', 'elasticpress' );
307
308 $this->output_success(
309 sprintf(
310 /* translators: 1: Indexable name */
311 $message_string,
312 esc_html( strtolower( $indexable->labels['plural'] ) )
313 )
314 );
315 }
316 }
317
318 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && ! empty( $this->index_meta['current_sync_item']['blog_id'] ) ) {
319 switch_to_blog( $this->index_meta['current_sync_item']['blog_id'] );
320 }
321
322 if ( $this->index_meta['current_sync_item']['put_mapping'] ) {
323 $this->put_mapping();
324 }
325
326 $this->index_objects();
327
328 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && ! empty( $this->index_meta['current_sync_item']['blog_id'] ) ) {
329 restore_current_blog();
330 }
331 }
332
333 /**
334 * Delete an index and recreate it sending the mapping.
335 *
336 * @since 4.0.0
337 */
338 protected function put_mapping() {
339 $this->index_meta['current_sync_item']['put_mapping'] = false;
340
341 /**
342 * Filter whether we should delete index and send new mapping at the start of the sync
343 *
344 * @since 2.1
345 * @hook ep_skip_index_reset
346 * @param {bool} $skip True means skip
347 * @param {array} $index_meta Current index meta
348 * @return {bool} New skip value
349 */
350 if ( apply_filters( 'ep_skip_index_reset', false, $this->index_meta ) ) {
351 return;
352 }
353
354 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
355
356 $indexable->delete_index();
357 $result = $indexable->put_mapping( 'raw' );
358
359 /**
360 * Fires after sync put mapping is completed
361 *
362 * @since 4.0.0
363 *
364 * @hook ep_sync_put_mapping
365 * @param {array} $index_meta Index meta information
366 * @param {Indexable} $indexable Indexable object
367 * @param {bool} $result Whether the request was successful or not
368 */
369 do_action( 'ep_sync_put_mapping', $this->index_meta, $indexable, $result );
370
371 /**
372 * Fires after dashboard put mapping is completed
373 *
374 * In this particular case, developer aiming a specific method should rely on
375 * `$index_meta['method']`, as historically `ep_dashboard_put_mapping` and
376 * `ep_cli_put_mapping` receive different parameters.
377 *
378 * @see Command::call_ep_cli_put_mapping()
379 *
380 * @since 2.1
381 * @hook ep_dashboard_put_mapping
382 * @param {array} $index_meta Index meta information
383 * @param {string} $status Current indexing status
384 */
385 do_action( 'ep_dashboard_put_mapping', $this->index_meta, 'start' );
386
387 if ( is_wp_error( $result ) ) {
388 $this->on_error_update_and_clean( array( 'message' => $result->get_error_message() ), 'mapping' );
389 return;
390 }
391
392 $index_exists = in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true );
393 if ( $index_exists ) {
394 $message = esc_html__( 'Mapping sent', 'elasticpress' );
395 } else {
396 $message = esc_html__( 'Index not present. Mapping sent', 'elasticpress' );
397 }
398
399 $this->output_success( $message );
400 }
401
402 /**
403 * Index documents of an index.
404 *
405 * @since 4.0.0
406 */
407 protected function index_objects() {
408 global $wp_actions;
409 // Hold original wp_actions.
410 $this->temporary_wp_actions = $wp_actions;
411
412 $this->current_query = $this->get_objects_to_index();
413
414 $this->index_meta['from'] = $this->index_meta['offset'];
415 $this->index_meta['found_items'] = (int) $this->current_query['total_objects'];
416 $this->index_meta['current_sync_item']['total'] = (int) $this->index_meta['current_sync_item']['found_items'];
417
418 if ( 'offset' === $this->index_meta['pagination_method'] ) {
419 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
420
421 if ( empty( $this->index_meta['current_sync_item']['shown_skip_message'] ) ) {
422 $this->index_meta['current_sync_item']['shown_skip_message'] = true;
423
424 $this->output(
425 sprintf(
426 /* translators: 1. Number of objects skipped 2. Indexable type */
427 esc_html__( 'Skipping %1$d %2$s…', 'elasticpress' ),
428 $this->index_meta['from'],
429 esc_html( strtolower( $indexable->labels['plural'] ) )
430 ),
431 'info',
432 'index_objects'
433 );
434 }
435 }
436
437 if ( $this->index_meta['found_items'] && $this->index_meta['offset'] < $this->index_meta['found_items'] ) {
438 $this->index_next_batch();
439 } else {
440 $this->index_cleanup();
441 }
442
443 usleep( 500 );
444
445 // Avoid running out of memory.
446 $this->stop_the_insanity();
447 }
448
449 /**
450 * Query the next objects to be indexed.
451 *
452 * @since 4.0.0
453 * @return array
454 */
455 protected function get_objects_to_index() {
456 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
457
458 /**
459 * Fires right before entries are about to be indexed.
460 *
461 * @since 4.0.0
462 *
463 * @hook ep_pre_sync_index
464 * @param {array} $args Args to query content with
465 */
466 do_action( 'ep_pre_sync_index', $this->index_meta, ( $this->index_meta['start'] ? 'start' : false ), $indexable );
467
468 /**
469 * Fires right before entries are about to be indexed.
470 *
471 * @since 2.1 Previously called only as 'ep_pre_dashboard_index'
472 * @since 4.0.0 Made available for all methods
473 *
474 * @hook ep_pre_{$index_method}_index
475 * @param {array} $args Args to query content with
476 */
477 do_action( "ep_pre_{$this->args['method']}_index", $this->index_meta, ( $this->index_meta['start'] ? 'start' : false ), $indexable );
478
479 $per_page = $this->get_index_default_per_page();
480
481 if ( ! empty( $this->args['per_page'] ) ) {
482 $per_page = $this->args['per_page'];
483 }
484
485 if ( ! empty( $this->args['nobulk'] ) ) {
486 $per_page = 1;
487 }
488
489 $args = [
490 'per_page' => absint( $per_page ),
491 'ep_sync_id' => uniqid(),
492 ];
493
494 if ( ! $indexable->support_indexing_advanced_pagination || 'offset' === $this->index_meta['pagination_method'] ) {
495 $args['offset'] = $this->index_meta['offset'];
496 }
497
498 if ( ! empty( $this->args['post-ids'] ) ) {
499 $args['include'] = $this->args['post-ids'];
500 }
501
502 if ( ! empty( $this->args['include'] ) ) {
503 $include = ( is_array( $this->args['include'] ) ) ? $this->args['include'] : explode( ',', str_replace( ' ', '', $this->args['include'] ) );
504 $args['include'] = array_map( 'absint', $include );
505 $args['per_page'] = count( $args['include'] );
506 }
507
508 if ( ! empty( $this->args['post_type'] ) ) {
509 $args['post_type'] = ( is_array( $this->args['post_type'] ) ) ? $this->args['post_type'] : explode( ',', $this->args['post_type'] );
510 $args['post_type'] = array_map( 'trim', $args['post_type'] );
511 }
512
513 // Start of advanced pagination arguments.
514 if ( ! empty( $this->args['upper_limit_object_id'] ) && is_numeric( $this->args['upper_limit_object_id'] ) ) {
515 $args['ep_indexing_upper_limit_object_id'] = $this->args['upper_limit_object_id'];
516 }
517
518 if ( ! empty( $this->args['lower_limit_object_id'] ) && is_numeric( $this->args['lower_limit_object_id'] ) ) {
519 $args['ep_indexing_lower_limit_object_id'] = $this->args['lower_limit_object_id'];
520 }
521
522 if ( ! empty( $this->index_meta['current_sync_item']['last_processed_object_id'] ) &&
523 is_numeric( $this->index_meta['current_sync_item']['last_processed_object_id'] )
524 ) {
525 $args['ep_indexing_last_processed_object_id'] = $this->index_meta['current_sync_item']['last_processed_object_id'];
526 }
527 // End of advanced pagination arguments.
528
529 /**
530 * Filters arguments used to query for content for each indexable
531 *
532 * @since 4.0.0
533 *
534 * @hook ep_sync_index_args
535 * @param {array} $args Args to query content with
536 * @return {array} New query args
537 */
538 $args = apply_filters( 'ep_sync_index_args', $args );
539
540 /**
541 * Filters arguments used to query for content for each indexable
542 *
543 * @since 3.0 Previously called only as 'ep_dashboard_index_args'
544 *
545 * @hook ep_{$index_method}_index_args
546 * @param {array} $args Args to query content with
547 * @return {array} New query args
548 */
549 $args = apply_filters( "ep_{$this->args['method']}_index_args", $args );
550
551 return $indexable->query_db( $args );
552 }
553
554 /**
555 * Index the next batch of documents.
556 *
557 * @since 4.0.0
558 */
559 protected function index_next_batch() {
560 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
561
562 /**
563 * Fires right before entries are about to be indexed in a dashboard sync
564 *
565 * @since 4.0.0
566 * @hook ep_pre_index_batch
567 * @param {array} $index_meta Index meta
568 */
569 do_action( 'ep_pre_index_batch', $this->index_meta );
570
571 $queued_items = [];
572
573 foreach ( $this->current_query['objects'] as $object ) {
574 if ( $this->should_skip_object_index( $object, $indexable ) ) {
575 $this->index_meta['current_sync_item']['skipped']++;
576 } else {
577 $queued_items[ $object->ID ] = true;
578 }
579 }
580
581 $this->index_meta['offset'] = absint( $this->index_meta['offset'] + count( $this->current_query['objects'] ) );
582
583 if ( ! empty( $queued_items ) ) {
584 $total_attempts = ( ! empty( $this->args['total_attempts'] ) ) ? absint( $this->args['total_attempts'] ) : 1;
585 $queued_items_ids = array_keys( $queued_items );
586
587 /**
588 * Filters the number of times the index will try before failing.
589 *
590 * @since 3.0
591 * @hook ep_index_batch_attempts_number
592 * @param {int} $total_attempts Number of attempts
593 * @return {int} New number of attempts
594 */
595 $total_attempts = apply_filters( 'ep_index_batch_attempts_number', $total_attempts );
596
597 for ( $attempts = 1; $attempts <= $total_attempts; $attempts++ ) {
598 $nobulk = ! empty( $this->args['nobulk'] );
599 $failed_objects = [];
600
601 /**
602 * Fires before each attempt of indexing objects
603 *
604 * @hook ep_index_batch_new_attempt
605 * @param {int} $attempts Current attempt
606 * @param {int} $total_attempts Total number of attempts
607 */
608 do_action( 'ep_index_batch_new_attempt', $attempts, $total_attempts );
609
610 $should_retry = false;
611
612 if ( $nobulk ) {
613 $object_id = reset( $queued_items_ids );
614 $return = $indexable->index( $object_id, true );
615
616 /**
617 * Fires after one by one indexing an object
618 *
619 * @since 4.0.0
620 *
621 * @hook ep_sync_object_index
622 * @param {int} $object_id Object to index
623 * @param {Indexable} $indexable Current indexable
624 * @param {mixed} $return Return of the index() call
625 */
626 do_action( 'ep_sync_object_index', $object_id, $indexable, $return );
627
628 /**
629 * Fires after one by one indexing an object
630 *
631 * @since 3.0 Previously called only as 'ep_cli_object_index'
632 * @since 4.0.0 Made available for all methods
633 *
634 * @hook ep_{$index_method}_object_index
635 * @param {int} $object_id Object to index
636 * @param {Indexable} $indexable Current indexable
637 * @param {mixed} $return Return of the index() call
638 */
639 do_action( "ep_{$this->args['method']}_object_index", $object_id, $indexable, $return );
640
641 if ( is_object( $return ) && ! empty( $return->error ) ) {
642 if ( ! empty( $return->error->reason ) ) {
643 $failed_objects[ $object->ID ] = (array) $return->error;
644 } else {
645 $failed_objects[ $object->ID ] = null;
646 }
647 }
648
649 if ( is_wp_error( $return ) ) {
650 $should_retry = true;
651 }
652 } else {
653 if ( ! empty( $this->args['static_bulk'] ) ) {
654 $bulk_requests = [ $indexable->bulk_index( $queued_items_ids ) ];
655 } else {
656 $bulk_requests = $indexable->bulk_index_dynamically( $queued_items_ids );
657 }
658
659 $failed_objects = [];
660 foreach ( $bulk_requests as $return ) {
661 /**
662 * Fires after bulk indexing
663 *
664 * @hook ep_cli_{indexable_slug}_bulk_index
665 * @param {array} $objects Objects being indexed
666 * @param {array} response Elasticsearch bulk index response
667 */
668 do_action( "ep_cli_{$indexable->slug}_bulk_index", $queued_items, $return );
669
670 if ( is_wp_error( $return ) ) {
671 $should_retry = true;
672 }
673 if ( is_array( $return ) && isset( $return['errors'] ) && true === $return['errors'] ) {
674 $failed_objects = array_merge(
675 $failed_objects,
676 array_filter(
677 $return['items'],
678 function( $item ) {
679 return ! empty( $item['index']['error'] );
680 }
681 )
682 );
683 }
684 }
685 }
686
687 // Things worked, we don't need to try again.
688 if ( ! $should_retry && ! count( $failed_objects ) ) {
689 break;
690 }
691 }
692
693 if ( is_wp_error( $return ) ) {
694 $this->index_meta['current_sync_item']['failed'] += count( $queued_items );
695
696 $wp_error_messages = $return->get_error_messages();
697
698 $this->maybe_process_error_limit(
699 count( $this->index_meta['current_sync_item']['errors'] ) + count( $wp_error_messages ),
700 count( $this->index_meta['current_sync_item']['errors'] ),
701 $wp_error_messages
702 );
703
704 $this->queue_message( $wp_error_messages, 'warning' );
705 } elseif ( count( $failed_objects ) ) {
706 $errors_output = $this->output_index_errors( $failed_objects );
707
708 $this->index_meta['current_sync_item']['synced'] += count( $queued_items ) - count( $failed_objects );
709
710 $this->maybe_process_error_limit(
711 $this->index_meta['current_sync_item']['failed'] + count( $failed_objects ),
712 $this->index_meta['current_sync_item']['failed'],
713 $errors_output
714 );
715
716 $this->index_meta['current_sync_item']['failed'] += count( $failed_objects );
717 $error_type = ! empty( $this->args['stop_on_error'] ) ? 'error' : 'warning';
718
719 $this->queue_message( $errors_output, $error_type );
720 } else {
721 $this->index_meta['current_sync_item']['synced'] += count( $queued_items );
722 }
723 }
724
725 $this->index_meta['current_sync_item']['last_processed_object_id'] = end( $this->current_query['objects'] )->ID;
726
727 $summary = sprintf(
728 /* translators: 1. Indexable type 2. Offset start, 3. Offset end, 4. Found items 5. Last object ID */
729 esc_html__( 'Processed %1$s %2$d - %3$d of %4$d. Last Object ID: %5$d', 'elasticpress' ),
730 esc_html( strtolower( $indexable->labels['plural'] ) ),
731 $this->index_meta['from'],
732 $this->index_meta['offset'],
733 $this->index_meta['found_items'],
734 $this->index_meta['current_sync_item']['last_processed_object_id']
735 );
736
737 $this->queue_message( $summary, 'info', 'index_next_batch' );
738 $this->flush_messages_queue();
739 }
740
741 /**
742 * If the number of errors is greater than the limit, slice the array to the limit.
743 * If the number of errors is less than or equal the limit, add the error message to the array (if it's not there).
744 * Merges the new errors with the existing errors.
745 *
746 * @since 4.5.1
747 * @param int $count Number of errors.
748 * @param int $num Number of errors to subtract from $limit.
749 * @param array $errors Array of errors.
750 */
751 protected function maybe_process_error_limit( $count, $num, $errors ) {
752 $error_store_msg = __( 'Reached maximum number of errors to store', 'elasticpress' );
753
754 /**
755 * Filter the number of errors of a current sync that should be stored.
756 *
757 * @since 4.5.1
758 * @hook ep_current_sync_number_of_errors_stored
759 * @param {int} $number Number of errors to be logged.
760 * @return {int} New value
761 */
762 $limit = (int) apply_filters( 'ep_current_sync_number_of_errors_stored', 50 );
763
764 if ( $limit > 0 && $count > $limit ) {
765 $diff = $limit - $num;
766 if ( $diff > 0 ) {
767 $errors = array_slice( $errors, 0, $diff );
768 } else {
769 $errors = [];
770 if ( end( $this->index_meta['current_sync_item']['errors'] ) !== $error_store_msg ) {
771 $this->index_meta['current_sync_item']['errors'][] = $error_store_msg;
772 }
773 }
774 }
775
776 $this->index_meta['current_sync_item']['errors'] = array_merge( $this->index_meta['current_sync_item']['errors'], $errors );
777 }
778
779 /**
780 * Update the sync info with the totals from the last sync item.
781 *
782 * @since 4.2.0
783 */
784 protected function update_totals_from_current_sync_item() {
785 $current_sync_item = $this->index_meta['current_sync_item'];
786
787 $errors = array_merge(
788 $this->index_meta['totals']['errors'],
789 $current_sync_item['errors']
790 );
791
792 /**
793 * Filter the number of errors of a sync that should be stored.
794 *
795 * @since 4.2.0
796 * @hook ep_sync_number_of_errors_stored
797 * @param {int} $number Number of errors to be logged.
798 * @return {int} New value
799 */
800 $logged_errors = (int) apply_filters( 'ep_sync_number_of_errors_stored', 50 );
801
802 $this->index_meta['totals']['total'] += $current_sync_item['total'];
803 $this->index_meta['totals']['synced'] += $current_sync_item['synced'];
804 $this->index_meta['totals']['skipped'] += $current_sync_item['skipped'];
805 $this->index_meta['totals']['failed'] += $current_sync_item['failed'];
806 $this->index_meta['totals']['errors'] = array_slice( $errors, $logged_errors * -1 );
807 }
808
809 /**
810 * Make the necessary clean up after a sync item of the stack was completely done.
811 *
812 * @since 4.0.0
813 * @return void
814 */
815 protected function index_cleanup() {
816 wp_reset_postdata();
817
818 $this->update_totals_from_current_sync_item();
819
820 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
821
822 $current_sync_item = $this->index_meta['current_sync_item'];
823
824 $this->index_meta['current_sync_item'] = null;
825
826 if ( $current_sync_item['failed'] ) {
827 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
828 $message = sprintf(
829 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of failed objects */
830 esc_html__( 'Number of %1$s index errors on site %2$d: %3$d', 'elasticpress' ),
831 esc_html( strtolower( $indexable->labels['plural'] ) ),
832 $current_sync_item['blog_id'],
833 $current_sync_item['failed']
834 );
835 } else {
836 $message = sprintf(
837 /* translators: 1: indexable (plural), 2: number of failed objects */
838 esc_html__( 'Number of %1$s index errors: %2$d', 'elasticpress' ),
839 esc_html( strtolower( $indexable->labels['plural'] ) ),
840 $current_sync_item['failed']
841 );
842 }
843
844 $this->output( $message, 'warning' );
845 }
846
847 $this->index_meta['offset'] = 0;
848
849 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
850 $message = sprintf(
851 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of synced objects */
852 esc_html__( 'Number of %1$s indexed on site %2$d: %3$d', 'elasticpress' ),
853 esc_html( strtolower( $indexable->labels['plural'] ) ),
854 $current_sync_item['blog_id'],
855 $current_sync_item['synced']
856 );
857 } else {
858 $message = sprintf(
859 /* translators: 1: indexable (plural), 2: number of synced objects */
860 esc_html__( 'Number of %1$s indexed: %2$d', 'elasticpress' ),
861 esc_html( strtolower( $indexable->labels['plural'] ) ),
862 $current_sync_item['synced']
863 );
864 }
865
866 $this->output_success( $message );
867 }
868
869 /**
870 * Update last sync info.
871 *
872 * @since 4.2.0
873 * @param string $final_status Optional final status
874 */
875 protected function update_last_index( string $final_status = '' ) {
876 $is_full_sync = $this->index_meta['put_mapping'];
877 $method = $this->index_meta['method'];
878 $start_time = $this->index_meta['start_time'];
879 $totals = $this->index_meta['totals'];
880 $trigger = $this->index_meta['trigger'];
881
882 $this->index_meta = null;
883
884 $end_date_time = date_create( 'now', wp_timezone() );
885 $start_time_sec = (int) $start_time;
886
887 // Time related info
888 $totals['end_date_time'] = $end_date_time ? $end_date_time->format( DATE_ATOM ) : false;
889 $totals['start_date_time'] = $start_time ? wp_date( DATE_ATOM, $start_time_sec ) : false;
890 $totals['end_time_gmt'] = time();
891 $totals['total_time'] = microtime( true ) - $start_time;
892
893 // Additional info
894 $totals['is_full_sync'] = $is_full_sync;
895 $totals['method'] = $method;
896 $totals['trigger'] = $trigger;
897
898 // Final status
899 if ( '' !== $final_status ) {
900 $totals['final_status'] = $final_status;
901 } elseif ( ! empty( $totals['failed'] ) ) {
902 $totals['final_status'] = 'with_errors';
903 } else {
904 $totals['final_status'] = 'success';
905 }
906
907 Utils\update_option( 'ep_last_cli_index', $totals, false );
908
909 $this->add_last_sync( $totals );
910 }
911
912 /**
913 * Add a sync to the list of all past syncs
914 *
915 * @since 5.0.0
916 * @param array $last_sync_info The latest sync info to be added to the log
917 * @return void
918 */
919 protected function add_last_sync( array $last_sync_info ) {
920 // Remove error messages from previous syncs - we only store msgs for the newest one.
921 $last_syncs = array_map(
922 function( $sync ) {
923 unset( $sync['errors'] );
924 return $sync;
925 },
926 $this->get_sync_history()
927 );
928
929 /**
930 * Filter the number of past syncs to keep info
931 *
932 * @since 5.0.0
933 * @hook ep_syncs_to_keep_info
934 * @param {int} $number Number of past syncs to keep info
935 * @return {int} New number
936 */
937 $syncs_to_keep = (int) apply_filters( 'ep_syncs_to_keep_info', 5 );
938
939 $last_syncs = array_slice( $last_syncs, 0, $syncs_to_keep - 1 );
940 array_unshift( $last_syncs, $last_sync_info );
941
942 Utils\update_option( 'ep_sync_history', $last_syncs, false );
943 }
944
945 /**
946 * Make the necessary clean up after everything was sync'd.
947 *
948 * @since 4.0.0
949 */
950 protected function full_index_complete() {
951 $this->update_last_index();
952
953 /**
954 * Fires after executing a reindex
955 *
956 * @since 4.0.0
957 * @hook ep_after_sync_index
958 */
959 do_action( 'ep_after_sync_index' );
960
961 /**
962 * Fires after executing a reindex
963 *
964 * @since 3.5.5 Previously called only as 'ep_after_dashboard_index'
965 * @since 4.0.0 Made available for all methods
966 * @hook ep_after_{$index_method}_index
967 */
968 do_action( "ep_after_{$this->args['method']}_index" );
969
970 $this->output_success( esc_html__( 'Sync complete', 'elasticpress' ) );
971 }
972
973 /**
974 * Check if network aliases need to be created.
975 *
976 * @since 4.0.0
977 * @return boolean
978 */
979 protected function has_network_alias_to_be_created() {
980 return count( $this->index_meta['network_alias'] ) > 0;
981 }
982
983 /**
984 * Create the next network alias.
985 *
986 * @since 4.0.0
987 */
988 protected function create_network_alias() {
989 $indexes = [];
990 $indexable = Indexables::factory()->get( array_shift( $this->index_meta['network_alias'] ) );
991
992 $sites = Utils\get_sites( 0, true );
993
994 foreach ( $sites as $site ) {
995 switch_to_blog( $site['blog_id'] );
996 $indexes[] = $indexable->get_index_name();
997 restore_current_blog();
998 }
999
1000 $result = $indexable->create_network_alias( $indexes );
1001
1002 if ( $result ) {
1003 $this->output_success(
1004 sprintf(
1005 /* translators: 1: Indexable name */
1006 esc_html__( 'Network alias created for %1$s', 'elasticpress' ),
1007 esc_html( strtolower( $indexable->labels['plural'] ) )
1008 )
1009 );
1010 } else {
1011 $this->output_error(
1012 sprintf(
1013 /* translators: 1: Indexable name */
1014 esc_html__( 'Network alias creation failed for %1$s', 'elasticpress' ),
1015 esc_html( strtolower( $indexable->labels['plural'] ) )
1016 )
1017 );
1018 }
1019 }
1020
1021 /**
1022 * Output a message.
1023 *
1024 * @since 4.0.0
1025 * @param string|array $message_text Message to be outputted
1026 * @param string $type Type of message
1027 * @param string $context Context of the output
1028 * @return void
1029 */
1030 protected function output( $message_text, $type = 'info', $context = '' ) {
1031 if ( $this->index_meta ) {
1032 Utils\update_option( 'ep_index_meta', $this->index_meta );
1033 } else {
1034 Utils\delete_option( 'ep_index_meta' );
1035 $totals = $this->get_last_sync();
1036 }
1037
1038 $message = [
1039 'message' => ( is_array( $message_text ) ) ? implode( "\n", $message_text ) : $message_text,
1040 'index_meta' => $this->index_meta,
1041 'totals' => $totals ?? [],
1042 'status' => $type,
1043 ];
1044
1045 if ( in_array( $type, [ 'warning', 'error' ], true ) ) {
1046 $message['errors'] = $this->build_message_errors_data( $message_text );
1047 }
1048
1049 if ( is_callable( $this->args['output_method'] ) ) {
1050 call_user_func( $this->args['output_method'], $message, $this->args, $this->index_meta, $context );
1051 }
1052 }
1053
1054 /**
1055 * Wrapper to the `output` method with a success message.
1056 *
1057 * @since 4.0.0
1058 * @param string $message Message string.
1059 * @param string $context Context of the output.
1060 */
1061 protected function output_success( $message, $context = '' ) {
1062 $this->output( $message, 'success', $context );
1063 }
1064
1065 /**
1066 * Wrapper to the `output` method with an error message.
1067 *
1068 * @since 4.0.0
1069 * @param string $message Message string.
1070 * @param string $context Context of the output.
1071 */
1072 protected function output_error( $message, $context = '' ) {
1073 $this->output( $message, 'error', $context );
1074 }
1075
1076 /**
1077 * Output index errors of failed objects.
1078 *
1079 * @since 4.0.0
1080 * @param array $failed_objects Failed objects
1081 */
1082 protected function output_index_errors( $failed_objects ) {
1083 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
1084
1085 $error_text = [];
1086
1087 foreach ( $failed_objects as $object ) {
1088 $error_text[] = ! empty( $object['index'] ) ? $object['index']['_id'] . ' (' . $indexable->labels['singular'] . '): [' . $object['index']['error']['type'] . '] ' . $object['index']['error']['reason'] : (string) $object;
1089 }
1090
1091 return $error_text;
1092 }
1093
1094 /**
1095 * Utilitary function to check if the indexable is being fully reindexed, i.e.,
1096 * the index was deleted, a new mapping was sent and content is being reindexed.
1097 *
1098 * @param string $indexable_slug Indexable slug.
1099 * @param int|null $blog_id Blog ID
1100 * @return boolean
1101 */
1102 public function is_full_reindexing( $indexable_slug, $blog_id = null ) {
1103 if ( empty( $this->index_meta ) || empty( $this->index_meta['put_mapping'] ) ) {
1104 /**
1105 * Filter if a fully reindex is being done to an indexable
1106 *
1107 * @since 4.0.0
1108 * @hook ep_is_full_reindexing_{$indexable_slug}
1109 * @param {bool} $is_full_reindexing If is fully reindexing
1110 * @return {bool} New value
1111 */
1112 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", false );
1113 }
1114
1115 $sync_stack = ( ! empty( $this->index_meta['sync_stack'] ) ) ? $this->index_meta['sync_stack'] : [];
1116 $current_sync_item = ( ! empty( $this->index_meta['current_sync_item'] ) ) ? $this->index_meta['current_sync_item'] : [];
1117
1118 $is_full_reindexing = false;
1119
1120 $all_items = $sync_stack;
1121 if ( ! empty( $current_sync_item ) ) {
1122 $all_items += [ $current_sync_item ];
1123 }
1124
1125 foreach ( $all_items as $sync_item ) {
1126 if ( $sync_item['indexable'] !== $indexable_slug ) {
1127 continue;
1128 }
1129
1130 if (
1131 ( empty( $sync_item['blog_id'] ) && ! $blog_id ) ||
1132 (int) $sync_item['blog_id'] === $blog_id
1133 ) {
1134 $is_full_reindexing = true;
1135 }
1136 }
1137
1138 /* this filter is documented above */
1139 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", $is_full_reindexing );
1140 }
1141
1142 /**
1143 * Get the previous syncs meta information.
1144 *
1145 * @since 5.0.0
1146 * @return array
1147 */
1148 public function get_sync_history() : array {
1149 return Utils\get_option( 'ep_sync_history', [] );
1150 }
1151
1152 /**
1153 * Get the last sync meta information.
1154 *
1155 * @since 5.0.0
1156 * @return array
1157 */
1158 public function get_last_sync() : array {
1159 $syncs = $this->get_sync_history();
1160 if ( empty( $syncs ) ) {
1161 return [];
1162 }
1163 return array_shift( $syncs );
1164 }
1165
1166 /**
1167 * Check if an object should be indexed or skipped.
1168 *
1169 * We used to have two different filters for this (one for the dashboard, another for CLI),
1170 * this method combines both.
1171 *
1172 * @param {stdClass} $object Object to be checked
1173 * @param {Indexable} $indexable Indexable
1174 * @return boolean
1175 */
1176 protected function should_skip_object_index( $object, $indexable ) {
1177 /**
1178 * Filter whether to not sync specific item in dashboard or not
1179 *
1180 * @since 2.1
1181 * @hook ep_item_sync_kill
1182 * @param {boolean} $kill False means dont sync
1183 * @param {array} $object Object to sync
1184 * @return {Indexable} Indexable that object belongs to
1185 */
1186 $ep_item_sync_kill = apply_filters( 'ep_item_sync_kill', false, $object, $indexable );
1187
1188 /**
1189 * Conditionally kill indexing for a post
1190 *
1191 * @hook ep_{indexable_slug}_index_kill
1192 * @param {bool} $index True means dont index
1193 * @param {int} $object_id Object ID
1194 * @return {bool} New value
1195 */
1196 $ep_indexable_sync_kill = apply_filters( 'ep_' . $indexable->slug . '_index_kill', false, $object->ID );
1197
1198 return $ep_item_sync_kill || $ep_indexable_sync_kill;
1199 }
1200
1201 /**
1202 * Given an array, create a new sync item and add it to the stack.
1203 *
1204 * @since 4.5.0
1205 * @param array $sync_stack_item The new sync item
1206 */
1207 protected function add_sync_item_to_stack( array $sync_stack_item ) {
1208 $indexable_slug = $sync_stack_item['indexable'];
1209 $indexable_object = Indexables::factory()->get( $indexable_slug );
1210
1211 if ( ! $indexable_object ) {
1212 return;
1213 }
1214
1215 $index_exists = in_array( $indexable_object->get_index_name(), $this->index_meta['starting_indices'], true );
1216
1217 $sync_stack_item['put_mapping'] = ! empty( $this->args['put_mapping'] ) || ! $index_exists;
1218
1219 if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
1220 array_unshift( $this->index_meta['sync_stack'], $sync_stack_item );
1221 return;
1222 }
1223
1224 // This is needed, because get_objects_to_index() calculates its total based on the current sync item.
1225 $this->index_meta['current_sync_item'] = $sync_stack_item;
1226
1227 $objects_to_index = $this->get_objects_to_index();
1228
1229 $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
1230
1231 $this->index_meta['sync_stack'][] = $sync_stack_item;
1232 }
1233
1234 /**
1235 * Processes an indexable that is not active.
1236 *
1237 * If running a full sync, delete the index of an unused indexable.
1238 *
1239 * @since 4.5.0
1240 */
1241 protected function process_not_active_indexable_sync_item() {
1242 $current_sync_item = $this->index_meta['current_sync_item'];
1243
1244 $this->index_meta['current_sync_item'] = null;
1245
1246 if ( empty( $current_sync_item['put_mapping'] ) ) {
1247 return;
1248 }
1249
1250 $indexable = Indexables::factory()->get( $current_sync_item['indexable'] );
1251
1252 if ( ! in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true ) ) {
1253 return;
1254 }
1255
1256 $indexable->delete_index();
1257
1258 $this->output_success(
1259 sprintf(
1260 /* translators: Index name */
1261 esc_html__( 'Index %s deleted', 'elasticpress' ),
1262 $indexable->get_index_name()
1263 )
1264 );
1265 }
1266
1267 /**
1268 * Resets some values to reduce memory footprint.
1269 */
1270 protected function stop_the_insanity() {
1271 global $wpdb, $wp_object_cache, $wp_actions;
1272
1273 $wpdb->queries = [];
1274
1275 /*
1276 * Runtime flushing was introduced in WordPress 6.0 and will flush only the
1277 * in-memory cache for persistent object caches
1278 */
1279 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
1280 wp_cache_flush_runtime();
1281 } else {
1282 /*
1283 * In the case where we're not using an external object cache, we need to call flush on the default
1284 * WordPress object cache class to clear the values from the cache property
1285 */
1286 if ( ! wp_using_ext_object_cache() ) {
1287 wp_cache_flush();
1288 }
1289 }
1290
1291 if ( is_object( $wp_object_cache ) ) {
1292 $wp_object_cache->group_ops = [];
1293 $wp_object_cache->stats = [];
1294 $wp_object_cache->memcache_debug = [];
1295
1296 // Make sure this is a public property, before trying to clear it.
1297 try {
1298 $cache_property = new \ReflectionProperty( $wp_object_cache, 'cache' );
1299 if ( $cache_property->isPublic() ) {
1300 $wp_object_cache->cache = [];
1301 }
1302 unset( $cache_property );
1303 } catch ( \ReflectionException $e ) {
1304 // No need to catch.
1305 }
1306
1307 if ( is_callable( $wp_object_cache, '__remoteset' ) ) {
1308 call_user_func( [ $wp_object_cache, '__remoteset' ] );
1309 }
1310 }
1311
1312 // Prevent wp_actions from growing out of control.
1313 // phpcs:disable
1314 $wp_actions = $this->temporary_wp_actions;
1315 // phpcs:enable
1316
1317 // It's high memory consuming as WP_Query instance holds all query results inside itself
1318 // and in theory $wp_filter will not stop growing until Out Of Memory exception occurs.
1319 remove_filter( 'get_term_metadata', [ wp_metadata_lazyloader(), 'lazyload_term_meta' ] );
1320
1321 /**
1322 * Fires after reducing the memory footprint
1323 *
1324 * @since 4.3.0
1325 * @hook ep_stop_the_insanity
1326 */
1327 do_action( 'ep_stop_the_insanity' );
1328 }
1329
1330 /**
1331 * Utilitary function to delete the index meta option.
1332 *
1333 * @since 4.0.0
1334 */
1335 public function clear_index_meta() {
1336 if ( ! empty( $this->index_meta ) ) {
1337 $this->update_last_index( 'aborted' );
1338 }
1339 $this->index_meta = false;
1340 Utils\delete_option( 'ep_index_meta', false );
1341 }
1342
1343 /**
1344 * Utilitary function to get the index meta option.
1345 *
1346 * @return array
1347 * @since 4.0.0
1348 */
1349 public function get_index_meta() {
1350 return Utils\get_option( 'ep_index_meta', [] );
1351 }
1352
1353 /**
1354 * Handle fatal errors during syncs.
1355 *
1356 * Added by register_shutdown_function. It will not be called if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1357 *
1358 * @since 4.2.0
1359 */
1360 public function handle_index_error() {
1361 $error = error_get_last();
1362 if ( empty( $error['type'] ) || E_ERROR !== $error['type'] ) {
1363 return;
1364 }
1365
1366 $this->on_error_update_and_clean( $error );
1367 }
1368
1369 /**
1370 * Handle fatal errors during syncs.
1371 *
1372 * Added via the `wp_php_error_message` filter. It will be called only if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1373 *
1374 * @since 4.2.0
1375 * @param bool $message HTML error message to display.
1376 * @param array $error Error information retrieved from error_get_last().
1377 * @return bool
1378 */
1379 public function wp_handle_index_error( $message, $error ) {
1380 $this->on_error_update_and_clean( $error );
1381 return $message;
1382 }
1383
1384 /**
1385 * Logs the error and clears the sync status, preventing the sync status from being stuck.
1386 *
1387 * @since 4.2.0
1388 * @param array $error Error information retrieved from error_get_last().
1389 * @param string $context Context of the error.
1390 */
1391 protected function on_error_update_and_clean( $error, $context = 'sync' ) {
1392 $this->update_totals_from_current_sync_item();
1393
1394 $totals = $this->index_meta['totals'];
1395
1396 $this->index_meta['totals']['errors'][] = $error['message'];
1397 $this->index_meta['totals']['failed'] = $totals['total'] - ( $totals['synced'] + $totals['skipped'] );
1398 $this->update_last_index( 'failed' );
1399
1400 /**
1401 * Fires after a sync failed due to a PHP fatal error.
1402 *
1403 * @since 4.2.0
1404 * @hook ep_after_sync_error
1405 * @param {array} $error The error
1406 */
1407 do_action( 'ep_after_sync_error', $error );
1408
1409 switch ( $context ) {
1410 case 'mapping':
1411 $message = sprintf(
1412 /* translators: Error message */
1413 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
1414 Utils\get_elasticsearch_error_reason( $error['message'] )
1415 );
1416 $message .= "\n";
1417 $message .= esc_html__( 'Mapping has failed, which will cause ElasticPress search results to be incorrect. Please click `Delete all Data and Start a Fresh Sync` to retry mapping.', 'elasticpress' );
1418 break;
1419 default:
1420 /* translators: Error message */
1421 $message = sprintf( esc_html__( 'Index failed: %s', 'elasticpress' ), $error['message'] );
1422 break;
1423 }
1424
1425 $this->output_error( $message );
1426 }
1427
1428 /**
1429 * Return the default number of documents to be sent to Elasticsearch on each batch.
1430 *
1431 * @since 4.4.0
1432 * @return integer
1433 */
1434 public function get_index_default_per_page() : int {
1435 /**
1436 * Filter number of items to index per cycle in the dashboard
1437 *
1438 * @since 2.1
1439 * @hook ep_index_default_per_page
1440 * @param {int} Entries per cycle
1441 * @return {int} New number of entries
1442 */
1443 return (int) apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
1444 }
1445
1446 /**
1447 * Add a message to the queue
1448 *
1449 * @since 4.7.0
1450 * @param string|array $message_text Message to be outputted
1451 * @param string $type Type of message
1452 * @param string $context Context of the output
1453 */
1454 protected function queue_message( $message_text, string $type, string $context = '' ) {
1455 $this->index_meta['messages_queue'][] = [
1456 'text' => $message_text,
1457 'type' => $type,
1458 'context' => $context,
1459 ];
1460 }
1461
1462 /**
1463 * Display messages in the queue.
1464 *
1465 * NOTE: As the dashboard sync exits after every output call (to respond the AJAX request),
1466 * this will just output one message. As the method is called every time the script is called,
1467 * all messages will be displayed but one at a time.
1468 *
1469 * @since 4.7.0
1470 */
1471 protected function flush_messages_queue() {
1472 if ( ! is_array( $this->index_meta['messages_queue'] ) ) {
1473 return;
1474 }
1475
1476 $messages_count = count( $this->index_meta['messages_queue'] );
1477 if ( 0 === $messages_count ) {
1478 return;
1479 }
1480
1481 for ( $i = 0; $i < $messages_count; $i++ ) {
1482 $next_message = array_shift( $this->index_meta['messages_queue'] );
1483 $this->output( $next_message['text'], $next_message['type'], $next_message['context'] );
1484 }
1485 }
1486
1487 /**
1488 * Get data for a given error message(s)
1489 *
1490 * @since 5.0.0
1491 * @param string|array $messages Messages
1492 * @return array
1493 */
1494 protected function build_message_errors_data( $messages ) : array {
1495 $messages = (array) $messages;
1496 $error_interpreter = new \ElasticPress\ElasticsearchErrorInterpreter();
1497
1498 $errors_list = [];
1499 foreach ( $messages as $message ) {
1500 $error = $error_interpreter->maybe_suggest_solution_for_es( $message );
1501
1502 if ( ! isset( $errors_list[ $error['error'] ] ) ) {
1503 $errors_list[ $error['error'] ] = [
1504 'solution' => $error['solution'],
1505 'count' => 1,
1506 ];
1507 } else {
1508 $errors_list[ $error['error'] ]['count']++;
1509 }
1510 }
1511 return $errors_list;
1512 }
1513
1514 /**
1515 * If this is a full sync, apply the draft feature settings
1516 *
1517 * @since 5.0.0
1518 */
1519 protected function maybe_apply_feature_settings() {
1520 if ( empty( $this->args['put_mapping'] ) ) {
1521 return;
1522 }
1523
1524 Features::factory()->apply_draft_feature_settings();
1525 }
1526
1527 /**
1528 * Return singleton instance of class.
1529 *
1530 * @return self
1531 * @since 4.0.0
1532 */
1533 public static function factory() {
1534 static $instance = false;
1535
1536 if ( ! $instance ) {
1537 $instance = new self();
1538 $instance->setup();
1539 }
1540
1541 return $instance;
1542 }
1543
1544 /**
1545 * DEPRECATED. Get the last index/sync meta information.
1546 *
1547 * @since 4.2.0
1548 * @deprecated 5.0.0
1549 * @return array
1550 */
1551 public function get_last_index() {
1552 _deprecated_function( __METHOD__, '5.0.0', '\ElasticPress\IndexHelper::get_last_sync' );
1553 return $this->get_last_sync();
1554 }
1555 }
1556