PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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.3.5, at includes/classes/IndexHelper.php

1,573 lines 46.7 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://www.elasticpress.io/resources/articles/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 $this->index_meta['offset'] = 0;
826
827 if ( $current_sync_item['failed'] ) {
828 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
829 $message = sprintf(
830 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of failed objects */
831 esc_html__( 'Number of %1$s index errors on site %2$d: %3$d', 'elasticpress' ),
832 esc_html( strtolower( $indexable->labels['plural'] ) ),
833 $current_sync_item['blog_id'],
834 $current_sync_item['failed']
835 );
836 } else {
837 $message = sprintf(
838 /* translators: 1: indexable (plural), 2: number of failed objects */
839 esc_html__( 'Number of %1$s index errors: %2$d', 'elasticpress' ),
840 esc_html( strtolower( $indexable->labels['plural'] ) ),
841 $current_sync_item['failed']
842 );
843 }
844
845 $this->output( $message, 'warning' );
846 }
847
848 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
849 $message = sprintf(
850 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of synced objects */
851 esc_html__( 'Number of %1$s indexed on site %2$d: %3$d', 'elasticpress' ),
852 esc_html( strtolower( $indexable->labels['plural'] ) ),
853 $current_sync_item['blog_id'],
854 $current_sync_item['synced']
855 );
856 } else {
857 $message = sprintf(
858 /* translators: 1: indexable (plural), 2: number of synced objects */
859 esc_html__( 'Number of %1$s indexed: %2$d', 'elasticpress' ),
860 esc_html( strtolower( $indexable->labels['plural'] ) ),
861 $current_sync_item['synced']
862 );
863 }
864
865 $this->output_success( $message );
866 }
867
868 /**
869 * Update last sync info.
870 *
871 * @since 4.2.0
872 * @param string $final_status Optional final status
873 */
874 protected function update_last_index( string $final_status = '' ) {
875 $is_full_sync = $this->index_meta['put_mapping'];
876 $method = $this->index_meta['method'];
877 $start_time = $this->index_meta['start_time'];
878 $totals = $this->index_meta['totals'];
879 $trigger = $this->index_meta['trigger'];
880
881 $this->index_meta = null;
882
883 $end_date_time = date_create( 'now', wp_timezone() );
884 $start_time_sec = (int) $start_time;
885
886 // Time related info
887 $totals['end_date_time'] = $end_date_time ? $end_date_time->format( DATE_ATOM ) : false;
888 $totals['start_date_time'] = $start_time ? wp_date( DATE_ATOM, $start_time_sec ) : false;
889 $totals['end_time_gmt'] = time();
890 $totals['total_time'] = microtime( true ) - $start_time;
891
892 // Additional info
893 $totals['is_full_sync'] = $is_full_sync;
894 $totals['method'] = $method;
895 $totals['trigger'] = $trigger;
896
897 // Final status
898 if ( '' !== $final_status ) {
899 $totals['final_status'] = $final_status;
900 } elseif ( ! empty( $totals['failed'] ) ) {
901 $totals['final_status'] = 'with_errors';
902 } else {
903 $totals['final_status'] = 'success';
904 }
905
906 Utils\update_option( 'ep_last_cli_index', $totals, false );
907
908 $this->add_last_sync( $totals );
909 }
910
911 /**
912 * Add a sync to the list of all past syncs
913 *
914 * @since 5.0.0
915 * @param array $last_sync_info The latest sync info to be added to the log
916 * @return void
917 */
918 protected function add_last_sync( array $last_sync_info ) {
919 // Remove error messages from previous syncs - we only store msgs for the newest one.
920 $last_syncs = array_map(
921 function ( $sync ) {
922 unset( $sync['errors'] );
923 return $sync;
924 },
925 $this->get_sync_history()
926 );
927
928 /**
929 * Filter the number of past syncs to keep info
930 *
931 * @since 5.0.0
932 * @hook ep_syncs_to_keep_info
933 * @param {int} $number Number of past syncs to keep info
934 * @return {int} New number
935 */
936 $syncs_to_keep = (int) apply_filters( 'ep_syncs_to_keep_info', 5 );
937
938 $last_syncs = array_slice( $last_syncs, 0, $syncs_to_keep - 1 );
939 array_unshift( $last_syncs, $last_sync_info );
940
941 Utils\update_option( 'ep_sync_history', $last_syncs, false );
942 }
943
944 /**
945 * Make the necessary clean up after everything was sync'd.
946 *
947 * @since 4.0.0
948 */
949 protected function full_index_complete() {
950 $this->update_last_index();
951
952 /**
953 * Fires after executing a reindex
954 *
955 * @since 4.0.0
956 * @param array $args Sync arguments.
957 * @hook ep_after_sync_index
958 */
959 do_action( 'ep_after_sync_index', $this->args );
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 * Utility 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} $indexable_object Object to be checked
1173 * @param {Indexable} $indexable Indexable
1174 * @return boolean
1175 */
1176 protected function should_skip_object_index( $indexable_object, $indexable ) {
1177 /**
1178 * Filter whether to not sync specific item in dashboard or not
1179 *
1180 * @since 2.1
1181 * @deprecated 5.3.3 Use ep_{indexable_slug}_sync_kill instead
1182 * @hook ep_item_sync_kill
1183 * @param {boolean} $kill False means dont sync
1184 * @param {array} $indexable_object Object to sync
1185 * @return {Indexable} Indexable that object belongs to
1186 */
1187 $ep_item_sync_kill = apply_filters_deprecated(
1188 'ep_item_sync_kill',
1189 [ false, $indexable_object, $indexable ],
1190 'ElasticPress 5.3.3',
1191 'ep_' . $indexable->slug . '_sync_kill'
1192 );
1193
1194 /** This filter is documented in includes/classes/Indexable.php */
1195 $ep_indexable_index_kill = apply_filters_deprecated(
1196 'ep_' . $indexable->slug . '_index_kill',
1197 [ false, $indexable_object->ID ],
1198 'ElasticPress 5.3.3',
1199 'ep_' . $indexable->slug . '_sync_kill'
1200 );
1201
1202 /** This filter is documented in includes/classes/Indexable.php */
1203 $ep_indexable_sync_kill = apply_filters( 'ep_' . $indexable->slug . '_sync_kill', false, $indexable_object->ID );
1204
1205 return $ep_item_sync_kill || $ep_indexable_sync_kill || $ep_indexable_index_kill;
1206 }
1207
1208 /**
1209 * Given an array, create a new sync item and add it to the stack.
1210 *
1211 * @since 4.5.0
1212 * @param array $sync_stack_item The new sync item
1213 */
1214 protected function add_sync_item_to_stack( array $sync_stack_item ) {
1215 $indexable_slug = $sync_stack_item['indexable'];
1216 $indexable_object = Indexables::factory()->get( $indexable_slug );
1217
1218 if ( ! $indexable_object ) {
1219 return;
1220 }
1221
1222 $index_exists = in_array( $indexable_object->get_index_name(), $this->index_meta['starting_indices'], true );
1223
1224 $sync_stack_item['put_mapping'] = ! empty( $this->args['put_mapping'] ) || ! $index_exists;
1225
1226 if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
1227 array_unshift( $this->index_meta['sync_stack'], $sync_stack_item );
1228 return;
1229 }
1230
1231 // This is needed, because get_objects_to_index() calculates its total based on the current sync item.
1232 $this->index_meta['current_sync_item'] = $sync_stack_item;
1233
1234 $objects_to_index = $this->get_objects_to_index();
1235
1236 $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
1237
1238 $this->index_meta['sync_stack'][] = $sync_stack_item;
1239 }
1240
1241 /**
1242 * Processes an indexable that is not active.
1243 *
1244 * If running a full sync, delete the index of an unused indexable.
1245 *
1246 * @since 4.5.0
1247 */
1248 protected function process_not_active_indexable_sync_item() {
1249 $current_sync_item = $this->index_meta['current_sync_item'];
1250
1251 $this->index_meta['current_sync_item'] = null;
1252
1253 if ( empty( $current_sync_item['put_mapping'] ) ) {
1254 return;
1255 }
1256
1257 $indexable = Indexables::factory()->get( $current_sync_item['indexable'] );
1258
1259 if ( ! in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true ) ) {
1260 return;
1261 }
1262
1263 $indexable->delete_index();
1264
1265 $this->output_success(
1266 sprintf(
1267 /* translators: Index name */
1268 esc_html__( 'Index %s deleted', 'elasticpress' ),
1269 $indexable->get_index_name()
1270 )
1271 );
1272 }
1273
1274 /**
1275 * Resets some values to reduce memory footprint.
1276 */
1277 protected function stop_the_insanity() {
1278 global $wpdb, $wp_object_cache, $wp_actions;
1279
1280 $wpdb->queries = [];
1281
1282 /*
1283 * Runtime flushing was introduced in WordPress 6.0 and will flush only the
1284 * in-memory cache for persistent object caches
1285 */
1286 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
1287 wp_cache_flush_runtime();
1288 } elseif ( ! wp_using_ext_object_cache() ) {
1289 /*
1290 * In the case where we're not using an external object cache, we need to call flush on the default
1291 * WordPress object cache class to clear the values from the cache property
1292 */
1293 wp_cache_flush();
1294 }
1295
1296 if ( is_object( $wp_object_cache ) ) {
1297 $wp_object_cache->group_ops = [];
1298 $wp_object_cache->stats = [];
1299 $wp_object_cache->memcache_debug = [];
1300
1301 // Make sure this is a public property, before trying to clear it.
1302 try {
1303 $cache_property = new \ReflectionProperty( $wp_object_cache, 'cache' );
1304 if ( $cache_property->isPublic() ) {
1305 $wp_object_cache->cache = [];
1306 }
1307 unset( $cache_property );
1308 } catch ( \ReflectionException $e ) {
1309 // No need to catch.
1310 }
1311
1312 if ( is_callable( $wp_object_cache, '__remoteset' ) ) {
1313 call_user_func( [ $wp_object_cache, '__remoteset' ] );
1314 }
1315 }
1316
1317 // Prevent wp_actions from growing out of control.
1318 // phpcs:disable
1319 $wp_actions = $this->temporary_wp_actions;
1320 // phpcs:enable
1321
1322 // It's high memory consuming as WP_Query instance holds all query results inside itself
1323 // and in theory $wp_filter will not stop growing until Out Of Memory exception occurs.
1324 remove_filter( 'get_term_metadata', [ wp_metadata_lazyloader(), 'lazyload_term_meta' ] );
1325
1326 /**
1327 * Fires after reducing the memory footprint
1328 *
1329 * @since 4.3.0
1330 * @hook ep_stop_the_insanity
1331 */
1332 do_action( 'ep_stop_the_insanity' );
1333 }
1334
1335 /**
1336 * Utility function to delete the index meta option.
1337 *
1338 * @since 4.0.0
1339 */
1340 public function clear_index_meta() {
1341 if ( ! empty( $this->index_meta ) ) {
1342 $this->update_last_index( 'aborted' );
1343 }
1344 $this->index_meta = false;
1345 Utils\delete_option( 'ep_index_meta', false );
1346 }
1347
1348 /**
1349 * Utility function to get the index meta option.
1350 *
1351 * @return array
1352 * @since 4.0.0
1353 */
1354 public function get_index_meta() {
1355 return Utils\get_option( 'ep_index_meta', [] );
1356 }
1357
1358 /**
1359 * Handle fatal errors during syncs.
1360 *
1361 * Added by register_shutdown_function. It will not be called if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1362 *
1363 * @since 4.2.0
1364 */
1365 public function handle_index_error() {
1366 $error = error_get_last();
1367 if ( empty( $error['type'] ) || E_ERROR !== $error['type'] ) {
1368 return;
1369 }
1370
1371 $this->on_error_update_and_clean( $error );
1372 }
1373
1374 /**
1375 * Handle fatal errors during syncs.
1376 *
1377 * Added via the `wp_php_error_message` filter. It will be called only if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1378 *
1379 * @since 4.2.0
1380 * @param bool $message HTML error message to display.
1381 * @param array $error Error information retrieved from error_get_last().
1382 * @return bool
1383 */
1384 public function wp_handle_index_error( $message, $error ) {
1385 $this->on_error_update_and_clean( $error );
1386 return $message;
1387 }
1388
1389 /**
1390 * Logs the error and clears the sync status, preventing the sync status from being stuck.
1391 *
1392 * @since 4.2.0
1393 * @param array $error Error information retrieved from error_get_last().
1394 * @param string $context Context of the error.
1395 */
1396 protected function on_error_update_and_clean( $error, $context = 'sync' ) {
1397 $this->update_totals_from_current_sync_item();
1398
1399 $totals = $this->index_meta['totals'];
1400
1401 $this->index_meta['totals']['errors'][] = $error['message'];
1402 $this->index_meta['totals']['failed'] = $totals['total'] - ( $totals['synced'] + $totals['skipped'] );
1403 $this->update_last_index( 'failed' );
1404
1405 /**
1406 * Fires after a sync failed due to a PHP fatal error.
1407 *
1408 * @since 4.2.0
1409 * @hook ep_after_sync_error
1410 * @param {array} $error The error
1411 */
1412 do_action( 'ep_after_sync_error', $error );
1413
1414 switch ( $context ) {
1415 case 'mapping':
1416 $message = sprintf(
1417 /* translators: Error message */
1418 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
1419 Utils\get_elasticsearch_error_reason( $error['message'] )
1420 );
1421 if ( $this->should_suggest_retry( $message ) ) {
1422 $message .= "\n";
1423 $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' );
1424 }
1425 break;
1426 default:
1427 /* translators: Error message */
1428 $message = sprintf( esc_html__( 'Index failed: %s', 'elasticpress' ), $error['message'] );
1429 break;
1430 }
1431
1432 $this->output_error( $message );
1433 }
1434
1435 /**
1436 * Return the default number of documents to be sent to Elasticsearch on each batch.
1437 *
1438 * @since 4.4.0
1439 * @return integer
1440 */
1441 public function get_index_default_per_page(): int {
1442 /**
1443 * Filter number of items to index per cycle in the dashboard
1444 *
1445 * @since 2.1
1446 * @hook ep_index_default_per_page
1447 * @param {int} Entries per cycle
1448 * @return {int} New number of entries
1449 */
1450 return (int) apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
1451 }
1452
1453 /**
1454 * Add a message to the queue
1455 *
1456 * @since 4.7.0
1457 * @param string|array $message_text Message to be outputted
1458 * @param string $type Type of message
1459 * @param string $context Context of the output
1460 */
1461 protected function queue_message( $message_text, string $type, string $context = '' ) {
1462 $this->index_meta['messages_queue'][] = [
1463 'text' => $message_text,
1464 'type' => $type,
1465 'context' => $context,
1466 ];
1467 }
1468
1469 /**
1470 * Display messages in the queue.
1471 *
1472 * NOTE: As the dashboard sync exits after every output call (to respond the AJAX request),
1473 * this will just output one message. As the method is called every time the script is called,
1474 * all messages will be displayed but one at a time.
1475 *
1476 * @since 4.7.0
1477 */
1478 protected function flush_messages_queue() {
1479 if ( ! is_array( $this->index_meta['messages_queue'] ) ) {
1480 return;
1481 }
1482
1483 $messages_count = count( $this->index_meta['messages_queue'] );
1484 if ( 0 === $messages_count ) {
1485 return;
1486 }
1487
1488 for ( $i = 0; $i < $messages_count; $i++ ) {
1489 $next_message = array_shift( $this->index_meta['messages_queue'] );
1490 $this->output( $next_message['text'], $next_message['type'], $next_message['context'] );
1491 }
1492 }
1493
1494 /**
1495 * Get data for a given error message(s)
1496 *
1497 * @since 5.0.0
1498 * @param string|array $messages Messages
1499 * @return array
1500 */
1501 protected function build_message_errors_data( $messages ): array {
1502 $messages = (array) $messages;
1503 $error_interpreter = new \ElasticPress\ElasticsearchErrorInterpreter();
1504
1505 $errors_list = [];
1506 foreach ( $messages as $message ) {
1507 $error = $error_interpreter->maybe_suggest_solution_for_es( $message );
1508
1509 if ( ! isset( $errors_list[ $error['error'] ] ) ) {
1510 $errors_list[ $error['error'] ] = [
1511 'solution' => $error['solution'],
1512 'count' => 1,
1513 ];
1514 } else {
1515 ++$errors_list[ $error['error'] ]['count'];
1516 }
1517 }
1518 return $errors_list;
1519 }
1520
1521 /**
1522 * If this is a full sync, apply the draft feature settings
1523 *
1524 * @since 5.0.0
1525 */
1526 protected function maybe_apply_feature_settings() {
1527 if ( empty( $this->args['put_mapping'] ) ) {
1528 return;
1529 }
1530
1531 Features::factory()->apply_draft_feature_settings();
1532 }
1533
1534 /**
1535 * Whether to suggest retrying the sync or not.
1536 *
1537 * @param string $message The message returned by the hosting server
1538 * @return boolean
1539 */
1540 protected function should_suggest_retry( $message ) {
1541 return ! preg_match( '/you have reached the limit of indices your plan supports/', $message );
1542 }
1543
1544 /**
1545 * Return singleton instance of class.
1546 *
1547 * @return self
1548 * @since 4.0.0
1549 */
1550 public static function factory() {
1551 static $instance = false;
1552
1553 if ( ! $instance ) {
1554 $instance = new self();
1555 $instance->setup();
1556 }
1557
1558 return $instance;
1559 }
1560
1561 /**
1562 * DEPRECATED. Get the last index/sync meta information.
1563 *
1564 * @since 4.2.0
1565 * @deprecated 5.0.0
1566 * @return array
1567 */
1568 public function get_last_index() {
1569 _deprecated_function( __METHOD__, '5.0.0', '\ElasticPress\IndexHelper::get_last_sync' );
1570 return $this->get_last_sync();
1571 }
1572 }
1573