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

SyncManager.php in ElasticPress 4.5.0, at includes/classes/Indexable/Post/SyncManager.php

877 lines 28.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Manage syncing of content between WP and Elasticsearch for posts
4 *
5 * @since 1.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Indexable\Post;
10
11 use ElasticPress\Elasticsearch as Elasticsearch;
12 use ElasticPress\Indexables as Indexables;
13 use ElasticPress\SyncManager as SyncManagerAbstract;
14 use ElasticPress\Utils;
15 use ElasticPress\IndexHelper;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 // @codeCoverageIgnoreStart
19 exit; // Exit if accessed directly.
20 // @codeCoverageIgnoreEnd
21 }
22
23 /**
24 * Sync manager class
25 */
26 class SyncManager extends SyncManagerAbstract {
27
28 /**
29 * Indexable slug
30 *
31 * @since 3.0
32 * @var string
33 */
34 public $indexable_slug = 'post';
35
36 /**
37 * Delete all post meta from other posts associated with a deleted post. Useful for attachments.
38 *
39 * @var bool
40 */
41 public $delete_all_meta = false;
42
43 /**
44 * Setup actions and filters
45 *
46 * @since 0.1.2
47 */
48 public function setup() {
49 if ( ! Elasticsearch::factory()->get_elasticsearch_version() ) {
50 return;
51 }
52
53 if ( ! $this->can_index_site() ) {
54 return;
55 }
56
57 add_action( 'wp_insert_post', array( $this, 'action_sync_on_update' ), 999, 3 );
58 add_action( 'add_attachment', array( $this, 'action_sync_on_update' ), 999, 3 );
59 add_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999, 3 );
60 add_action( 'delete_post', array( $this, 'action_delete_post' ) );
61 add_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
62 add_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
63 // Called just because we need to know somehow if $delete_all is set before action_queue_meta_sync() runs.
64 add_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ), 10, 5 );
65 add_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
66 add_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );
67
68 add_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
69 add_filter( 'ep_sync_delete_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
70
71 // Conditionally update posts associated with terms
72 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_edit_single_term' ] );
73 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_term_list_screen' ] );
74 add_action( 'set_object_terms', array( $this, 'action_set_object_terms' ), 10, 6 );
75 add_action( 'edited_term', array( $this, 'action_edited_term' ), 10, 3 );
76 add_action( 'deleted_term_relationships', array( $this, 'action_deleted_term_relationships' ), 10, 3 );
77
78 // Clear field limit cache
79 add_action( 'ep_update_index_settings', [ $this, 'clear_total_fields_limit_cache' ] );
80 add_action( 'ep_sync_put_mapping', [ $this, 'clear_total_fields_limit_cache' ] );
81 add_action( 'ep_saved_weighting_configuration', [ $this, 'clear_total_fields_limit_cache' ] );
82
83 // Clear distinct meta field per post type cache
84 add_action( 'wp_insert_post', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_post_id' ] );
85 add_action( 'delete_post', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_post_id' ] );
86 add_action( 'updated_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
87 add_action( 'added_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
88 add_action( 'deleted_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
89 add_action( 'delete_post_metadata', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
90 }
91
92 /**
93 * Un-setup actions and filters (for multisite).
94 *
95 * @since 4.0
96 */
97 public function tear_down() {
98 remove_action( 'wp_insert_post', array( $this, 'action_sync_on_update' ), 999 );
99 remove_action( 'add_attachment', array( $this, 'action_sync_on_update' ), 999 );
100 remove_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999 );
101 remove_action( 'delete_post', array( $this, 'action_delete_post' ) );
102 remove_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ) );
103 remove_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ) );
104 remove_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ) );
105 remove_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ) );
106 remove_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );
107 remove_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
108 remove_filter( 'ep_sync_delete_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
109 }
110
111 /**
112 * Whether to delete all meta from other posts that is associated with the deleted post.
113 *
114 * @param bool $check Whether to allow metadata deletion of the given type.
115 * @param int $object_id ID of the object metadata is for.
116 * @param string $meta_key Metadata key.
117 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
118 * @param bool $delete_all Whether to delete the matching metadata entries
119 * for all objects, ignoring the specified $object_id
120 * @return bool
121 */
122 public function maybe_delete_meta_for_all( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
123 $this->delete_all_meta = $delete_all;
124 return $check;
125 }
126
127 /**
128 * Filter to allow cron and WP CLI processes to index/delete documents
129 *
130 * @param boolean $bypass The current filtered value
131 * @return boolean Boolean indicating if permission checking should be bypased or not
132 * @since 3.6.0
133 */
134 public function filter_bypass_permission_checks_for_machines( $bypass ) {
135 // Allow index/delete during cron
136 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
137 return true;
138 }
139
140 // Allow index/delete during WP CLI commands
141 if ( defined( 'WP_CLI' ) && WP_CLI ) {
142 return true;
143 }
144
145 return $bypass;
146 }
147
148 /**
149 * When whitelisted meta is updated, queue the post for reindex
150 *
151 * @param int|array $meta_id Meta id.
152 * @param int $object_id Object id.
153 * @param string $meta_key Meta key.
154 * @param string $meta_value Meta value.
155 * @since 2.0
156 */
157 public function action_queue_meta_sync( $meta_id, $object_id, $meta_key, $meta_value ) {
158 if ( $this->kill_sync() ) {
159 return;
160 }
161
162 $indexable = Indexables::factory()->get( $this->indexable_slug );
163
164 /**
165 * Filter to whether skip a sync during autosave, defaults to true
166 *
167 * @hook ep_skip_autosave_sync
168 * @since 4.3.0
169 * @param {bool} $skip True means to disable sync for autosaves
170 * @param {string} $function Function applying filter
171 * @return {boolean} New value
172 */
173 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
174 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
175 // Bypass saving if doing autosave
176 // @codeCoverageIgnoreStart
177 return;
178 // @codeCoverageIgnoreEnd
179 }
180 }
181
182 $post = get_post( $object_id );
183
184 /**
185 * Filter to allow skipping a sync triggered by meta changes
186 *
187 * @hook ep_skip_post_meta_sync
188 * @param {bool} $skip True means kill sync for post
189 * @param {WP_Post} $post The post that's attempting to be synced
190 * @param {int} $meta_id ID of the meta that triggered the sync
191 * @param {string} $meta_key The key of the meta that triggered the sync
192 * @param {string} $meta_value The value of the meta that triggered the sync
193 * @return {boolean} New value
194 */
195 if ( apply_filters( 'ep_skip_post_meta_sync', false, $post, $meta_id, $meta_key, $meta_value ) ) {
196 return;
197 }
198
199 if ( empty( $object_id ) && $this->delete_all_meta ) {
200 add_filter( 'ep_is_integrated_request', '__return_true' );
201
202 $query = new \WP_Query(
203 [
204 'ep_integrate' => true,
205 'meta_key' => $meta_key,
206 'meta_value' => $meta_value,
207 'fields' => 'ids',
208 'post_type' => $indexable->get_indexable_post_types(),
209 ]
210 );
211
212 remove_filter( 'ep_is_integrated_request', '__return_true' );
213
214 if ( $query->have_posts() && $query->elasticsearch_success ) {
215 $posts_to_be_synced = array_filter(
216 $query->posts,
217 function( $object_id ) {
218 return ! apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id );
219 }
220 );
221 if ( ! empty( $posts_to_be_synced ) ) {
222 $indexable->bulk_index( $posts_to_be_synced );
223 }
224 }
225 } else {
226 $indexable_post_statuses = $indexable->get_indexable_post_status();
227 $post_type = get_post_type( $object_id );
228
229 $is_meta_allowed = $indexable->is_meta_allowed( $meta_key, $post );
230 if ( ! $is_meta_allowed ) {
231 return;
232 }
233
234 if ( in_array( $post->post_status, $indexable_post_statuses, true ) ) {
235 $indexable_post_types = $indexable->get_indexable_post_types();
236
237 if ( in_array( $post_type, $indexable_post_types, true ) ) {
238 /**
239 * Filter to kill post sync
240 *
241 * @hook ep_post_sync_kill
242 * @param {bool} $skip True meanas kill sync for post
243 * @param {int} $object_id ID of post
244 * @param {int} $object_id ID of post
245 * @return {boolean} New value
246 */
247 if ( apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id ) ) {
248 return;
249 }
250
251 $this->add_to_queue( $object_id );
252 }
253 }
254 }
255 }
256
257 /**
258 * Delete ES post when WP post is deleted
259 *
260 * @param int $post_id Post id.
261 * @since 0.1.0
262 */
263 public function action_delete_post( $post_id ) {
264 if ( $this->kill_sync() ) {
265 return;
266 }
267
268 /**
269 * Filter whether to skip the permissions check on deleting a post
270 *
271 * @hook ep_sync_delete_permissions_bypass
272 * @param {bool} $bypass True to bypass
273 * @param {int} $post_id ID of post
274 * @return {boolean} New value
275 */
276 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_delete_permissions_bypass', false, $post_id ) ) {
277 return;
278 }
279
280 $indexable = Indexables::factory()->get( $this->indexable_slug );
281 $post_type = get_post_type( $post_id );
282
283 $indexable_post_types = $indexable->get_indexable_post_types();
284
285 if ( ! in_array( $post_type, $indexable_post_types, true ) ) {
286 // If not an indexable post type, skip delete.
287 return;
288 }
289
290 Indexables::factory()->get( $this->indexable_slug )->delete( $post_id, false );
291
292 /**
293 * Make sure to remove this post from the sync queue in case an shutdown happens
294 * before a redirect when a redirect has already been triggered.
295 */
296 if ( isset( $this->sync_queue[ $post_id ] ) ) {
297 unset( $this->sync_queue[ $post_id ] );
298 }
299 }
300
301 /**
302 * Sync ES index with what happened to the post being saved
303 *
304 * @param int $post_id Post id.
305 * @since 0.1.0
306 */
307 public function action_sync_on_update( $post_id ) {
308 if ( $this->kill_sync() ) {
309 return;
310 }
311
312 $indexable = Indexables::factory()->get( $this->indexable_slug );
313 $post_type = get_post_type( $post_id );
314
315 /**
316 * Filter to whether skip a sync during autosave, defaults to true
317 *
318 * @hook ep_skip_autosave_sync
319 * @since 4.3.0
320 * @param {bool} $skip True means to disable sync for autosaves
321 * @param {string} $function Function applying filter
322 * @return {boolean} New value
323 */
324 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
325 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
326 // Bypass saving if doing autosave
327 // @codeCoverageIgnoreStart
328 return;
329 // @codeCoverageIgnoreEnd
330 }
331 }
332
333 /**
334 * Filter whether to skip the permissions check on updating a post
335 *
336 * @hook ep_sync_insert_permissions_bypass
337 * @param {bool} $bypass True to bypass
338 * @param {int} $post_id ID of post
339 * @return {boolean} New value
340 */
341 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_insert_permissions_bypass', false, $post_id ) ) {
342 return;
343 }
344
345 $post = get_post( $post_id );
346
347 $indexable_post_statuses = $indexable->get_indexable_post_status();
348
349 // Our post was published, but is no longer, so let's remove it from the Elasticsearch index.
350 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
351 $this->action_delete_post( $post_id );
352 } else {
353 $indexable_post_types = $indexable->get_indexable_post_types();
354
355 if ( in_array( $post_type, $indexable_post_types, true ) ) {
356 /**
357 * Fire before post is queued for syncing
358 *
359 * @hook ep_sync_on_transition
360 * @param {int} $post_id ID of post
361 */
362 do_action( 'ep_sync_on_transition', $post_id );
363
364 /**
365 * Filter to kill post sync
366 *
367 * @hook ep_post_sync_kill
368 * @param {bool} $skip True means kill sync for post
369 * @param {int} $object_id ID of post
370 * @param {int} $object_id ID of post
371 * @return {boolean} New value
372 */
373 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
374 return;
375 }
376
377 $this->add_to_queue( $post_id );
378 }
379 }
380 }
381
382 /**
383 * Depending on the number of posts associated with the term display an admin notice
384 *
385 * @since 4.4.0
386 * @param array $notices Current ElasticPress admin notices
387 * @return array
388 */
389 public function maybe_display_notice_edit_single_term( $notices ) {
390 global $pagenow, $tag;
391
392 /**
393 * Make sure we're on a term-related page in the admin dashboard.
394 */
395 if ( ! is_admin() || 'term.php' !== $pagenow || ! $tag instanceof \WP_Term ) {
396 return $notices;
397 }
398
399 if ( IndexHelper::factory()->get_index_default_per_page() >= $tag->count ) {
400
401 $child_tags = get_term_children( $tag->term_id, $tag->taxonomy );
402 if ( empty( $child_tags ) ) {
403 return $notices;
404 }
405 foreach ( $child_tags as $child_tag_id ) {
406 $child_tag = get_term( $child_tag_id );
407 if ( ! is_wp_error( $child_tag ) && IndexHelper::factory()->get_index_default_per_page() < $child_tag->count && ! isset( $notices['edited_single_parent_term'] ) ) {
408 $notices['edited_single_parent_term'] = [
409 'html' => sprintf(
410 /* translators: Sync Page URL */
411 __( 'Due to the number of posts associated with its child terms, you will need to <a href="%s">resync</a> after editing or deleting it.', 'elasticpress' ),
412 Utils\get_sync_url()
413 ),
414 'type' => 'warning',
415 'dismiss' => true,
416 ];
417 break;
418 }
419 }
420
421 return $notices;
422 }
423 $notices['edited_single_term'] = [
424 'html' => sprintf(
425 /* translators: Sync Page URL */
426 __( 'Due to the number of posts associated with this term, you will need to <a href="%s">resync</a> after editing or deleting it.', 'elasticpress' ),
427 Utils\get_sync_url()
428 ),
429 'type' => 'warning',
430 'dismiss' => true,
431 ];
432
433 return $notices;
434 }
435
436 /**
437 * Depending on the number of posts display an admin notice in the Dashboard Terms List Screen
438 *
439 * @since 4.4.0
440 * @param array $notices Current ElasticPress admin notices
441 * @return array
442 */
443 public function maybe_display_notice_term_list_screen( $notices ) {
444 global $pagenow, $tax;
445
446 /**
447 * Make sure we're on a term-related page in the admin dashboard.
448 */
449 if ( ! is_admin() || 'edit-tags.php' !== $pagenow || ! $tax instanceof \WP_Taxonomy ) {
450 return $notices;
451 }
452
453 if ( ! $this->is_tax_max_count_bigger_than_items_per_cycle( $tax ) ) {
454 return $notices;
455 }
456
457 $notices['too_many_posts_on_term'] = [
458 'html' => sprintf(
459 /* translators: Sync Page URL */
460 __( 'Depending on the number of posts associated with a term, you may need to <a href="%s">resync</a> after editing or deleting it.', 'elasticpress' ),
461 Utils\get_sync_url()
462 ),
463 'type' => 'warning',
464 'dismiss' => true,
465 ];
466
467 return $notices;
468 }
469
470 /**
471 * When a post's terms are changed, re-index.
472 *
473 * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
474 * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
475 * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
476 * hence why we also have `action_deleted_term_relationships`.
477 *
478 * @see set_object_terms
479 * @param int $post_id Post ID.
480 * @param array $terms An array of object terms.
481 * @param array $tt_ids An array of term taxonomy IDs.
482 * @param string $taxonomy Taxonomy slug.
483 * @param bool $append Whether to append new terms to the old terms.
484 * @param array $old_tt_ids Old array of term taxonomy IDs.
485 * @since 4.0.0
486 */
487 public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
488 if ( $this->kill_sync() ) {
489 return;
490 }
491
492 /**
493 * Filter to whether skip a sync during autosave, defaults to true
494 *
495 * @hook ep_skip_autosave_sync
496 * @since 4.3.0
497 * @param {bool} $skip True means to disable sync for autosaves
498 * @param {string} $function Function applying filter
499 * @return {boolean} New value
500 */
501 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
502 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
503 // Bypass saving if doing autosave
504 // @codeCoverageIgnoreStart
505 return;
506 // @codeCoverageIgnoreEnd
507 }
508 }
509
510 /**
511 * Filter to allow skipping this action in case of custom handling
512 *
513 * @hook ep_skip_action_set_object_terms
514 * @param {bool} $skip True means kill sync for post
515 * @param {int} $post_id ID of post
516 * @param {array} $terms An array of object terms.
517 * @param {array} $tt_ids An array of term taxonomy IDs.
518 * @param {string} $taxonomy Taxonomy slug.
519 * @param {bool} $append Whether to append new terms to the old terms.
520 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
521 * @return {boolean} New value
522 */
523 if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
524 return;
525 }
526
527 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
528 return;
529 }
530
531 /**
532 * Fire before post is queued for syncing
533 *
534 * @since 4.0.0
535 * @hook ep_sync_on_set_object_terms
536 * @param {int} $post_id ID of post
537 * @param {array} $terms An array of object terms.
538 * @param {array} $tt_ids An array of term taxonomy IDs.
539 * @param {string} $taxonomy Taxonomy slug.
540 * @param {bool} $append Whether to append new terms to the old terms.
541 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
542 */
543 do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
544
545 $this->add_to_queue( $post_id );
546 }
547
548 /**
549 * When a term is updated, re-index all posts attached to that term
550 *
551 * @param int $term_id Term id.
552 * @param int $tt_id Term Taxonomy id.
553 * @param string $taxonomy Taxonomy name.
554 * @since 4.0.0
555 */
556 public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
557 global $wpdb;
558
559 /**
560 * Filter to whether skip a sync during autosave, defaults to true
561 *
562 * @hook ep_skip_autosave_sync
563 * @since 4.3.0
564 * @param {bool} $skip True means to disable sync for autosaves
565 * @param {string} $function Function applying filter
566 * @return {boolean} New value
567 */
568 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
569 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
570 // Bypass saving if doing autosave
571 // @codeCoverageIgnoreStart
572 return;
573 // @codeCoverageIgnoreEnd
574 }
575 }
576
577 // Find ID of all attached posts (query lifted from wp_delete_term())
578 $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d", $tt_id ) );
579
580 // If the current term is not attached, check if the child terms are attached to the post
581 if ( empty( $object_ids ) ) {
582 $child_terms = get_term_children( $term_id, $taxonomy );
583 if ( ! empty( $child_terms ) ) {
584 $in_id = join( ',', array_fill( 0, count( $child_terms ), '%d' ) );
585 $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( {$in_id} )", $child_terms ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
586 }
587 }
588 if ( ! count( $object_ids ) ) {
589 return;
590 }
591
592 // If we have more items to update than the number set as Content Items per Index Cycle, skip it.
593 $should_skip = count( $object_ids ) > IndexHelper::factory()->get_index_default_per_page();
594
595 /**
596 * Filter to allow skipping this action in case of custom handling
597 *
598 * @hook ep_skip_action_edited_term
599 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
600 * @param {int} $term_id Term id.
601 * @param {int} $tt_id Term Taxonomy id.
602 * @param {string} $taxonomy Taxonomy name.
603 * @param {array} $object_ids IDs of the objects attached to the term id.
604 * @return {bool} New value of whether to skip running action_edited_term or not
605 */
606 if ( apply_filters( 'ep_skip_action_edited_term', $should_skip, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
607 return;
608 }
609
610 // Add all of them to the queue
611 foreach ( $object_ids as $post_id ) {
612 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
613 continue;
614 }
615
616 /**
617 * Fire before post is queued for syncing
618 *
619 * @hook ep_sync_on_edited_term
620 * @param {int} $post_id ID of post
621 * @param {int} $term_id ID of the term that was edited
622 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
623 * @param {int} $taxonomy Taxonomy of the term that was edited
624 */
625 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
626
627 $this->add_to_queue( $post_id );
628 }
629 }
630
631 /**
632 * When a term relationship is deleted, re-index all posts attached to that term
633 *
634 * @param int $post_id Post ID.
635 * @param array $tt_ids An array of term taxonomy IDs.
636 * @param string $taxonomy Taxonomy slug.
637 * @since 4.0.0
638 */
639 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
640 if ( $this->kill_sync() ) {
641 return;
642 }
643
644 /**
645 * Filter to whether skip a sync during autosave, defaults to true
646 *
647 * @hook ep_skip_autosave_sync
648 * @since 4.3.0
649 * @param {bool} $skip True means to disable sync for autosaves
650 * @param {string} $function Function applying filter
651 * @return {boolean} New value
652 */
653 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
654 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
655 // Bypass saving if doing autosave
656 // @codeCoverageIgnoreStart
657 return;
658 // @codeCoverageIgnoreEnd
659 }
660 }
661
662 /**
663 * Filter to allow skipping this action in case of custom handling
664 *
665 * @hook ep_skip_action_deleted_term_relationships
666 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
667 * @param {int} $post_id Post ID.
668 * @param {array} $tt_ids An array of term taxonomy IDs.
669 * @param {string} $taxonomy Taxonomy slug.
670 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
671 */
672 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
673 return;
674 }
675
676 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
677 return;
678 }
679
680 /**
681 * Fire before post is queued for syncing
682 *
683 * @hook ep_sync_on_deleted_term_relationships
684 * @since 4.0.0
685 * @param {int} $post_id ID of post
686 * @param {array} $tt_ids An array of term taxonomy IDs.
687 * @param {string} $taxonomy Taxonomy of the term that was edited
688 */
689 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
690
691 $this->add_to_queue( $post_id );
692 }
693
694 /**
695 * Create mapping and network alias when a new blog is created.
696 *
697 * @param WP_Site $blog New site object.
698 */
699 public function action_create_blog_index( $blog ) {
700 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
701 // @codeCoverageIgnoreStart
702 return;
703 // @codeCoverageIgnoreEnd
704 }
705
706 if ( $this->kill_sync() ) {
707 return;
708 }
709
710 $non_global_indexable_objects = Indexables::factory()->get_all( false );
711
712 switch_to_blog( $blog->blog_id );
713
714 foreach ( $non_global_indexable_objects as $indexable ) {
715 $indexable->delete_index();
716 $indexable->put_mapping();
717
718 $index_name = $indexable->get_index_name( $blog->blog_id );
719 $indexable->create_network_alias( [ $index_name ] );
720 }
721
722 restore_current_blog();
723 }
724
725 /**
726 * Clear the cache of the total fields limit
727 *
728 * @since 4.4.0
729 */
730 public function clear_total_fields_limit_cache() {
731 $indexable = Indexables::factory()->get( $this->indexable_slug );
732 $cache_key = 'ep_total_fields_limit_' . $indexable->get_index_name();
733
734 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
735 delete_site_transient( $cache_key );
736 } else {
737 delete_transient( $cache_key );
738 }
739 }
740
741 /**
742 * Clear the cache of the total fields limit
743 *
744 * @param int $post_id The post ID
745 * @since 4.4.0
746 */
747 public function clear_meta_keys_db_per_post_type_cache_by_post_id( $post_id ) {
748 $post_type = get_post_type( $post_id );
749 if ( $post_type ) {
750 $this->clear_meta_keys_db_cache( $post_type );
751 }
752 }
753
754 /**
755 * Clear the cache of the total fields limit
756 *
757 * @param int|array $meta_id Meta ID
758 * @param int $post_id The post ID
759 * @since 4.4.0
760 */
761 public function clear_meta_keys_db_per_post_type_cache_by_meta( $meta_id, $post_id ) {
762 $post_type = get_post_type( $post_id );
763 if ( $post_type ) {
764 $this->clear_meta_keys_db_cache( $post_type );
765 }
766 }
767
768 /**
769 * Clear the cache of the total fields limit
770 *
771 * @param string $post_type The post type
772 * @since 4.4.0
773 */
774 protected function clear_meta_keys_db_cache( $post_type ) {
775 delete_transient( 'ep_meta_field_keys' );
776 delete_transient( 'ep_meta_field_keys_' . $post_type );
777 }
778
779 /**
780 * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
781 *
782 * @param int $post_id The post ID.
783 * @param string $taxonomy The taxonomy slug.
784 * @return boolean
785 */
786 protected function should_reindex_post( $post_id, $taxonomy ) {
787 /**
788 * Filter to kill post sync
789 *
790 * @hook ep_post_sync_kill
791 * @param {bool} $skip True meanas kill sync for post
792 * @param {int} $object_id ID of post
793 * @param {int} $object_id ID of post
794 * @return {boolean} New value
795 */
796 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
797 return false;
798 }
799
800 $post = get_post( $post_id );
801 if ( ! is_object( $post ) ) {
802 return false;
803 }
804
805 $indexable = Indexables::factory()->get( $this->indexable_slug );
806
807 // Check post status
808 $indexable_post_statuses = $indexable->get_indexable_post_status();
809 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
810 return false;
811 }
812
813 // Only re-index if the taxonomy is indexed for this post
814 $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
815 $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
816 if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
817 return false;
818 }
819
820 // Check post type
821 $indexable_post_types = $indexable->get_indexable_post_types();
822 if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
823 return false;
824 }
825
826 // If we have more items to update than the number set as Content Items per Index Cycle, skip it to avoid a timeout.
827 $single_ids_queued = array_unique( array_keys( $this->sync_queue ) );
828 $has_too_many_queued = count( $single_ids_queued ) > IndexHelper::factory()->get_index_default_per_page();
829
830 return ! $has_too_many_queued;
831 }
832
833 /**
834 * Given a taxonomy, check if the term with most posts is under or above the number set as Content Items per Index Cycle.
835 *
836 * The result will be cached in a transient. Its TTL will depend on the result:
837 * If it is determined we have a term with more posts, cache it for more time.
838 *
839 * @since 4.4.0
840 * @param \WP_Taxonomy $tax The taxonomy object
841 * @return boolean
842 */
843 protected function is_tax_max_count_bigger_than_items_per_cycle( \WP_Taxonomy $tax ) : bool {
844 $transient_name = "ep_term_max_count_{$tax->name}";
845 $cached_max_count = get_transient( $transient_name );
846
847 if ( is_integer( $cached_max_count ) ) {
848 return $cached_max_count > IndexHelper::factory()->get_index_default_per_page();
849 }
850
851 $max_count = get_terms(
852 [
853 'taxonomy' => $tax->name,
854 'orderby' => 'count',
855 'order' => 'DESC',
856 'number' => 1,
857 'count' => true,
858 ]
859 );
860
861 if ( ! is_array( $max_count ) || ! count( $max_count ) || ! $max_count[0] instanceof \WP_Term || ! is_integer( $max_count[0]->count ) ) {
862 set_transient( $transient_name, 0, HOUR_IN_SECONDS );
863 return false;
864 }
865
866 $is_max_count_bigger = $max_count[0]->count > IndexHelper::factory()->get_index_default_per_page();
867
868 set_transient(
869 $transient_name,
870 $max_count[0]->count,
871 $is_max_count_bigger ? DAY_IN_SECONDS : HOUR_IN_SECONDS
872 );
873
874 return $is_max_count_bigger;
875 }
876 }
877