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

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

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