PluginProbe
ElasticPress / 4.4.0
ElasticPress v4.4.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.4.0, at includes/classes/Indexable/Post/SyncManager.php

848 lines 27.1 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 ]
209 );
210
211 remove_filter( 'ep_is_integrated_request', '__return_true' );
212
213 if ( $query->have_posts() && $query->elasticsearch_success ) {
214 $posts_to_be_synced = array_filter(
215 $query->posts,
216 function( $object_id ) {
217 return ! apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id );
218 }
219 );
220 if ( ! empty( $posts_to_be_synced ) ) {
221 $indexable->bulk_index( $posts_to_be_synced );
222 }
223 }
224 } else {
225 $indexable_post_statuses = $indexable->get_indexable_post_status();
226 $post_type = get_post_type( $object_id );
227
228 $is_meta_allowed = $indexable->is_meta_allowed( $meta_key, $post );
229 if ( ! $is_meta_allowed ) {
230 return;
231 }
232
233 if ( in_array( $post->post_status, $indexable_post_statuses, true ) ) {
234 $indexable_post_types = $indexable->get_indexable_post_types();
235
236 if ( in_array( $post_type, $indexable_post_types, true ) ) {
237 /**
238 * Filter to kill post sync
239 *
240 * @hook ep_post_sync_kill
241 * @param {bool} $skip True meanas kill sync for post
242 * @param {int} $object_id ID of post
243 * @param {int} $object_id ID of post
244 * @return {boolean} New value
245 */
246 if ( apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id ) ) {
247 return;
248 }
249
250 $this->add_to_queue( $object_id );
251 }
252 }
253 }
254 }
255
256 /**
257 * Delete ES post when WP post is deleted
258 *
259 * @param int $post_id Post id.
260 * @since 0.1.0
261 */
262 public function action_delete_post( $post_id ) {
263 if ( $this->kill_sync() ) {
264 return;
265 }
266
267 /**
268 * Filter whether to skip the permissions check on deleting a post
269 *
270 * @hook ep_sync_delete_permissions_bypass
271 * @param {bool} $bypass True to bypass
272 * @param {int} $post_id ID of post
273 * @return {boolean} New value
274 */
275 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_delete_permissions_bypass', false, $post_id ) ) {
276 return;
277 }
278
279 $indexable = Indexables::factory()->get( $this->indexable_slug );
280 $post_type = get_post_type( $post_id );
281
282 $indexable_post_types = $indexable->get_indexable_post_types();
283
284 if ( ! in_array( $post_type, $indexable_post_types, true ) ) {
285 // If not an indexable post type, skip delete.
286 return;
287 }
288
289 Indexables::factory()->get( $this->indexable_slug )->delete( $post_id, false );
290
291 /**
292 * Make sure to remove this post from the sync queue in case an shutdown happens
293 * before a redirect when a redirect has already been triggered.
294 */
295 if ( isset( $this->sync_queue[ $post_id ] ) ) {
296 unset( $this->sync_queue[ $post_id ] );
297 }
298 }
299
300 /**
301 * Sync ES index with what happened to the post being saved
302 *
303 * @param int $post_id Post id.
304 * @since 0.1.0
305 */
306 public function action_sync_on_update( $post_id ) {
307 if ( $this->kill_sync() ) {
308 return;
309 }
310
311 $indexable = Indexables::factory()->get( $this->indexable_slug );
312 $post_type = get_post_type( $post_id );
313
314 /**
315 * Filter to whether skip a sync during autosave, defaults to true
316 *
317 * @hook ep_skip_autosave_sync
318 * @since 4.3.0
319 * @param {bool} $skip True means to disable sync for autosaves
320 * @param {string} $function Function applying filter
321 * @return {boolean} New value
322 */
323 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
324 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
325 // Bypass saving if doing autosave
326 // @codeCoverageIgnoreStart
327 return;
328 // @codeCoverageIgnoreEnd
329 }
330 }
331
332 /**
333 * Filter whether to skip the permissions check on updating a post
334 *
335 * @hook ep_sync_insert_permissions_bypass
336 * @param {bool} $bypass True to bypass
337 * @param {int} $post_id ID of post
338 * @return {boolean} New value
339 */
340 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_insert_permissions_bypass', false, $post_id ) ) {
341 return;
342 }
343
344 $post = get_post( $post_id );
345
346 $indexable_post_statuses = $indexable->get_indexable_post_status();
347
348 // Our post was published, but is no longer, so let's remove it from the Elasticsearch index.
349 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
350 $this->action_delete_post( $post_id );
351 } else {
352 $indexable_post_types = $indexable->get_indexable_post_types();
353
354 if ( in_array( $post_type, $indexable_post_types, true ) ) {
355 /**
356 * Fire before post is queued for syncing
357 *
358 * @hook ep_sync_on_transition
359 * @param {int} $post_id ID of post
360 */
361 do_action( 'ep_sync_on_transition', $post_id );
362
363 /**
364 * Filter to kill post sync
365 *
366 * @hook ep_post_sync_kill
367 * @param {bool} $skip True means kill sync for post
368 * @param {int} $object_id ID of post
369 * @param {int} $object_id ID of post
370 * @return {boolean} New value
371 */
372 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
373 return;
374 }
375
376 $this->add_to_queue( $post_id );
377 }
378 }
379 }
380
381 /**
382 * Depending on the number of posts associated with the term display an admin notice
383 *
384 * @since 4.4.0
385 * @param array $notices Current ElasticPress admin notices
386 * @return array
387 */
388 public function maybe_display_notice_edit_single_term( $notices ) {
389 global $pagenow, $tag;
390
391 /**
392 * Make sure we're on a term-related page in the admin dashboard.
393 */
394 if ( ! is_admin() || 'term.php' !== $pagenow || ! $tag instanceof \WP_Term ) {
395 return $notices;
396 }
397
398 if ( IndexHelper::factory()->get_index_default_per_page() >= $tag->count ) {
399 return $notices;
400 }
401
402 $notices['edited_single_term'] = [
403 'html' => sprintf(
404 /* translators: Sync Page URL */
405 __( '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' ),
406 Utils\get_sync_url()
407 ),
408 'type' => 'warning',
409 'dismiss' => true,
410 ];
411
412 return $notices;
413 }
414
415 /**
416 * Depending on the number of posts display an admin notice in the Dashboard Terms List Screen
417 *
418 * @since 4.4.0
419 * @param array $notices Current ElasticPress admin notices
420 * @return array
421 */
422 public function maybe_display_notice_term_list_screen( $notices ) {
423 global $pagenow, $tax;
424
425 /**
426 * Make sure we're on a term-related page in the admin dashboard.
427 */
428 if ( ! is_admin() || 'edit-tags.php' !== $pagenow || ! $tax instanceof \WP_Taxonomy ) {
429 return $notices;
430 }
431
432 if ( ! $this->is_tax_max_count_bigger_than_items_per_cycle( $tax ) ) {
433 return $notices;
434 }
435
436 $notices['too_many_posts_on_term'] = [
437 'html' => sprintf(
438 /* translators: Sync Page URL */
439 __( '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' ),
440 Utils\get_sync_url()
441 ),
442 'type' => 'warning',
443 'dismiss' => true,
444 ];
445
446 return $notices;
447 }
448
449 /**
450 * When a post's terms are changed, re-index.
451 *
452 * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
453 * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
454 * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
455 * hence why we also have `action_deleted_term_relationships`.
456 *
457 * @see set_object_terms
458 * @param int $post_id Post ID.
459 * @param array $terms An array of object terms.
460 * @param array $tt_ids An array of term taxonomy IDs.
461 * @param string $taxonomy Taxonomy slug.
462 * @param bool $append Whether to append new terms to the old terms.
463 * @param array $old_tt_ids Old array of term taxonomy IDs.
464 * @since 4.0.0
465 */
466 public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
467 if ( $this->kill_sync() ) {
468 return;
469 }
470
471 /**
472 * Filter to whether skip a sync during autosave, defaults to true
473 *
474 * @hook ep_skip_autosave_sync
475 * @since 4.3.0
476 * @param {bool} $skip True means to disable sync for autosaves
477 * @param {string} $function Function applying filter
478 * @return {boolean} New value
479 */
480 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
481 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
482 // Bypass saving if doing autosave
483 // @codeCoverageIgnoreStart
484 return;
485 // @codeCoverageIgnoreEnd
486 }
487 }
488
489 /**
490 * Filter to allow skipping this action in case of custom handling
491 *
492 * @hook ep_skip_action_set_object_terms
493 * @param {bool} $skip True means kill sync for post
494 * @param {int} $post_id ID of post
495 * @param {array} $terms An array of object terms.
496 * @param {array} $tt_ids An array of term taxonomy IDs.
497 * @param {string} $taxonomy Taxonomy slug.
498 * @param {bool} $append Whether to append new terms to the old terms.
499 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
500 * @return {boolean} New value
501 */
502 if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
503 return;
504 }
505
506 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
507 return;
508 }
509
510 /**
511 * Fire before post is queued for syncing
512 *
513 * @since 4.0.0
514 * @hook ep_sync_on_set_object_terms
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 */
522 do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
523
524 $this->add_to_queue( $post_id );
525 }
526
527 /**
528 * When a term is updated, re-index all posts attached to that term
529 *
530 * @param int $term_id Term id.
531 * @param int $tt_id Term Taxonomy id.
532 * @param string $taxonomy Taxonomy name.
533 * @since 4.0.0
534 */
535 public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
536 global $wpdb;
537
538 /**
539 * Filter to whether skip a sync during autosave, defaults to true
540 *
541 * @hook ep_skip_autosave_sync
542 * @since 4.3.0
543 * @param {bool} $skip True means to disable sync for autosaves
544 * @param {string} $function Function applying filter
545 * @return {boolean} New value
546 */
547 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
548 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
549 // Bypass saving if doing autosave
550 // @codeCoverageIgnoreStart
551 return;
552 // @codeCoverageIgnoreEnd
553 }
554 }
555
556 // Find ID of all attached posts (query lifted from wp_delete_term())
557 $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) );
558
559 if ( ! count( $object_ids ) ) {
560 return;
561 }
562
563 // If we have more items to update than the number set as Content Items per Index Cycle, skip it.
564 $should_skip = count( $object_ids ) > IndexHelper::factory()->get_index_default_per_page();
565
566 /**
567 * Filter to allow skipping this action in case of custom handling
568 *
569 * @hook ep_skip_action_edited_term
570 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
571 * @param {int} $term_id Term id.
572 * @param {int} $tt_id Term Taxonomy id.
573 * @param {string} $taxonomy Taxonomy name.
574 * @param {array} $object_ids IDs of the objects attached to the term id.
575 * @return {bool} New value of whether to skip running action_edited_term or not
576 */
577 if ( apply_filters( 'ep_skip_action_edited_term', $should_skip, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
578 return;
579 }
580
581 // Add all of them to the queue
582 foreach ( $object_ids as $post_id ) {
583 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
584 continue;
585 }
586
587 /**
588 * Fire before post is queued for syncing
589 *
590 * @hook ep_sync_on_edited_term
591 * @param {int} $post_id ID of post
592 * @param {int} $term_id ID of the term that was edited
593 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
594 * @param {int} $taxonomy Taxonomy of the term that was edited
595 */
596 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
597
598 $this->add_to_queue( $post_id );
599 }
600 }
601
602 /**
603 * When a term relationship is deleted, re-index all posts attached to that term
604 *
605 * @param int $post_id Post ID.
606 * @param array $tt_ids An array of term taxonomy IDs.
607 * @param string $taxonomy Taxonomy slug.
608 * @since 4.0.0
609 */
610 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
611 if ( $this->kill_sync() ) {
612 return;
613 }
614
615 /**
616 * Filter to whether skip a sync during autosave, defaults to true
617 *
618 * @hook ep_skip_autosave_sync
619 * @since 4.3.0
620 * @param {bool} $skip True means to disable sync for autosaves
621 * @param {string} $function Function applying filter
622 * @return {boolean} New value
623 */
624 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
625 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
626 // Bypass saving if doing autosave
627 // @codeCoverageIgnoreStart
628 return;
629 // @codeCoverageIgnoreEnd
630 }
631 }
632
633 /**
634 * Filter to allow skipping this action in case of custom handling
635 *
636 * @hook ep_skip_action_deleted_term_relationships
637 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
638 * @param {int} $post_id Post ID.
639 * @param {array} $tt_ids An array of term taxonomy IDs.
640 * @param {string} $taxonomy Taxonomy slug.
641 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
642 */
643 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
644 return;
645 }
646
647 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
648 return;
649 }
650
651 /**
652 * Fire before post is queued for syncing
653 *
654 * @hook ep_sync_on_deleted_term_relationships
655 * @since 4.0.0
656 * @param {int} $post_id ID of post
657 * @param {array} $tt_ids An array of term taxonomy IDs.
658 * @param {string} $taxonomy Taxonomy of the term that was edited
659 */
660 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
661
662 $this->add_to_queue( $post_id );
663 }
664
665 /**
666 * Create mapping and network alias when a new blog is created.
667 *
668 * @param WP_Site $blog New site object.
669 */
670 public function action_create_blog_index( $blog ) {
671 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
672 // @codeCoverageIgnoreStart
673 return;
674 // @codeCoverageIgnoreEnd
675 }
676
677 if ( $this->kill_sync() ) {
678 return;
679 }
680
681 $non_global_indexable_objects = Indexables::factory()->get_all( false );
682
683 switch_to_blog( $blog->blog_id );
684
685 foreach ( $non_global_indexable_objects as $indexable ) {
686 $indexable->delete_index();
687 $indexable->put_mapping();
688
689 $index_name = $indexable->get_index_name( $blog->blog_id );
690 $indexable->create_network_alias( [ $index_name ] );
691 }
692
693 restore_current_blog();
694 }
695
696 /**
697 * Clear the cache of the total fields limit
698 *
699 * @since 4.4.0
700 */
701 public function clear_total_fields_limit_cache() {
702 $indexable = Indexables::factory()->get( $this->indexable_slug );
703 $cache_key = 'ep_total_fields_limit_' . $indexable->get_index_name();
704
705 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
706 delete_site_transient( $cache_key );
707 } else {
708 delete_transient( $cache_key );
709 }
710 }
711
712 /**
713 * Clear the cache of the total fields limit
714 *
715 * @param int $post_id The post ID
716 * @since 4.4.0
717 */
718 public function clear_meta_keys_db_per_post_type_cache_by_post_id( $post_id ) {
719 $post_type = get_post_type( $post_id );
720 if ( $post_type ) {
721 $this->clear_meta_keys_db_cache( $post_type );
722 }
723 }
724
725 /**
726 * Clear the cache of the total fields limit
727 *
728 * @param int|array $meta_id Meta ID
729 * @param int $post_id The post ID
730 * @since 4.4.0
731 */
732 public function clear_meta_keys_db_per_post_type_cache_by_meta( $meta_id, $post_id ) {
733 $post_type = get_post_type( $post_id );
734 if ( $post_type ) {
735 $this->clear_meta_keys_db_cache( $post_type );
736 }
737 }
738
739 /**
740 * Clear the cache of the total fields limit
741 *
742 * @param string $post_type The post type
743 * @since 4.4.0
744 */
745 protected function clear_meta_keys_db_cache( $post_type ) {
746 delete_transient( 'ep_meta_field_keys' );
747 delete_transient( 'ep_meta_field_keys_' . $post_type );
748 }
749
750 /**
751 * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
752 *
753 * @param int $post_id The post ID.
754 * @param string $taxonomy The taxonomy slug.
755 * @return boolean
756 */
757 protected function should_reindex_post( $post_id, $taxonomy ) {
758 /**
759 * Filter to kill post sync
760 *
761 * @hook ep_post_sync_kill
762 * @param {bool} $skip True meanas kill sync for post
763 * @param {int} $object_id ID of post
764 * @param {int} $object_id ID of post
765 * @return {boolean} New value
766 */
767 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
768 return false;
769 }
770
771 $post = get_post( $post_id );
772 if ( ! is_object( $post ) ) {
773 return false;
774 }
775
776 $indexable = Indexables::factory()->get( $this->indexable_slug );
777
778 // Check post status
779 $indexable_post_statuses = $indexable->get_indexable_post_status();
780 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
781 return false;
782 }
783
784 // Only re-index if the taxonomy is indexed for this post
785 $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
786 $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
787 if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
788 return false;
789 }
790
791 // Check post type
792 $indexable_post_types = $indexable->get_indexable_post_types();
793 if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
794 return false;
795 }
796
797 // If we have more items to update than the number set as Content Items per Index Cycle, skip it to avoid a timeout.
798 $single_ids_queued = array_unique( array_keys( $this->sync_queue ) );
799 $has_too_many_queued = count( $single_ids_queued ) > IndexHelper::factory()->get_index_default_per_page();
800
801 return ! $has_too_many_queued;
802 }
803
804 /**
805 * Given a taxonomy, check if the term with most posts is under or above the number set as Content Items per Index Cycle.
806 *
807 * The result will be cached in a transient. Its TTL will depend on the result:
808 * If it is determined we have a term with more posts, cache it for more time.
809 *
810 * @since 4.4.0
811 * @param \WP_Taxonomy $tax The taxonomy object
812 * @return boolean
813 */
814 protected function is_tax_max_count_bigger_than_items_per_cycle( \WP_Taxonomy $tax ) : bool {
815 $transient_name = "ep_term_max_count_{$tax->name}";
816 $cached_max_count = get_transient( $transient_name );
817
818 if ( is_integer( $cached_max_count ) ) {
819 return $cached_max_count > IndexHelper::factory()->get_index_default_per_page();
820 }
821
822 $max_count = get_terms(
823 [
824 'taxonomy' => $tax->name,
825 'orderby' => 'count',
826 'order' => 'DESC',
827 'number' => 1,
828 'count' => true,
829 ]
830 );
831
832 if ( ! is_array( $max_count ) || ! count( $max_count ) || ! $max_count[0] instanceof \WP_Term || ! is_integer( $max_count[0]->count ) ) {
833 set_transient( $transient_name, 0, HOUR_IN_SECONDS );
834 return false;
835 }
836
837 $is_max_count_bigger = $max_count[0]->count > IndexHelper::factory()->get_index_default_per_page();
838
839 set_transient(
840 $transient_name,
841 $max_count[0]->count,
842 $is_max_count_bigger ? DAY_IN_SECONDS : HOUR_IN_SECONDS
843 );
844
845 return $is_max_count_bigger;
846 }
847 }
848