PluginProbe
ElasticPress / 4.5.2
ElasticPress v4.5.2
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.2, at includes/classes/Indexable/Post/SyncManager.php

881 lines 28.6 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 if ( $this->kill_sync() ) {
560 return;
561 }
562
563 /**
564 * Filter to whether skip a sync during autosave, defaults to true
565 *
566 * @hook ep_skip_autosave_sync
567 * @since 4.3.0
568 * @param {bool} $skip True means to disable sync for autosaves
569 * @param {string} $function Function applying filter
570 * @return {boolean} New value
571 */
572 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
573 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
574 // Bypass saving if doing autosave
575 // @codeCoverageIgnoreStart
576 return;
577 // @codeCoverageIgnoreEnd
578 }
579 }
580
581 // Find ID of all attached posts (query lifted from wp_delete_term())
582 $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d", $tt_id ) );
583
584 // If the current term is not attached, check if the child terms are attached to the post
585 if ( empty( $object_ids ) ) {
586 $child_terms = get_term_children( $term_id, $taxonomy );
587 if ( ! empty( $child_terms ) ) {
588 $in_id = join( ',', array_fill( 0, count( $child_terms ), '%d' ) );
589 $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
590 }
591 }
592 if ( ! count( $object_ids ) ) {
593 return;
594 }
595
596 // If we have more items to update than the number set as Content Items per Index Cycle, skip it.
597 $should_skip = count( $object_ids ) > IndexHelper::factory()->get_index_default_per_page();
598
599 /**
600 * Filter to allow skipping this action in case of custom handling
601 *
602 * @hook ep_skip_action_edited_term
603 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
604 * @param {int} $term_id Term id.
605 * @param {int} $tt_id Term Taxonomy id.
606 * @param {string} $taxonomy Taxonomy name.
607 * @param {array} $object_ids IDs of the objects attached to the term id.
608 * @return {bool} New value of whether to skip running action_edited_term or not
609 */
610 if ( apply_filters( 'ep_skip_action_edited_term', $should_skip, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
611 return;
612 }
613
614 // Add all of them to the queue
615 foreach ( $object_ids as $post_id ) {
616 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
617 continue;
618 }
619
620 /**
621 * Fire before post is queued for syncing
622 *
623 * @hook ep_sync_on_edited_term
624 * @param {int} $post_id ID of post
625 * @param {int} $term_id ID of the term that was edited
626 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
627 * @param {int} $taxonomy Taxonomy of the term that was edited
628 */
629 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
630
631 $this->add_to_queue( $post_id );
632 }
633 }
634
635 /**
636 * When a term relationship is deleted, re-index all posts attached to that term
637 *
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 * @since 4.0.0
642 */
643 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
644 if ( $this->kill_sync() ) {
645 return;
646 }
647
648 /**
649 * Filter to whether skip a sync during autosave, defaults to true
650 *
651 * @hook ep_skip_autosave_sync
652 * @since 4.3.0
653 * @param {bool} $skip True means to disable sync for autosaves
654 * @param {string} $function Function applying filter
655 * @return {boolean} New value
656 */
657 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
658 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
659 // Bypass saving if doing autosave
660 // @codeCoverageIgnoreStart
661 return;
662 // @codeCoverageIgnoreEnd
663 }
664 }
665
666 /**
667 * Filter to allow skipping this action in case of custom handling
668 *
669 * @hook ep_skip_action_deleted_term_relationships
670 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
671 * @param {int} $post_id Post ID.
672 * @param {array} $tt_ids An array of term taxonomy IDs.
673 * @param {string} $taxonomy Taxonomy slug.
674 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
675 */
676 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
677 return;
678 }
679
680 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
681 return;
682 }
683
684 /**
685 * Fire before post is queued for syncing
686 *
687 * @hook ep_sync_on_deleted_term_relationships
688 * @since 4.0.0
689 * @param {int} $post_id ID of post
690 * @param {array} $tt_ids An array of term taxonomy IDs.
691 * @param {string} $taxonomy Taxonomy of the term that was edited
692 */
693 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
694
695 $this->add_to_queue( $post_id );
696 }
697
698 /**
699 * Create mapping and network alias when a new blog is created.
700 *
701 * @param WP_Site $blog New site object.
702 */
703 public function action_create_blog_index( $blog ) {
704 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
705 // @codeCoverageIgnoreStart
706 return;
707 // @codeCoverageIgnoreEnd
708 }
709
710 if ( $this->kill_sync() ) {
711 return;
712 }
713
714 $non_global_indexable_objects = Indexables::factory()->get_all( false );
715
716 switch_to_blog( $blog->blog_id );
717
718 foreach ( $non_global_indexable_objects as $indexable ) {
719 $indexable->delete_index();
720 $indexable->put_mapping();
721
722 $index_name = $indexable->get_index_name( $blog->blog_id );
723 $indexable->create_network_alias( [ $index_name ] );
724 }
725
726 restore_current_blog();
727 }
728
729 /**
730 * Clear the cache of the total fields limit
731 *
732 * @since 4.4.0
733 */
734 public function clear_total_fields_limit_cache() {
735 $indexable = Indexables::factory()->get( $this->indexable_slug );
736 $cache_key = 'ep_total_fields_limit_' . $indexable->get_index_name();
737
738 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
739 delete_site_transient( $cache_key );
740 } else {
741 delete_transient( $cache_key );
742 }
743 }
744
745 /**
746 * Clear the cache of the total fields limit
747 *
748 * @param int $post_id The post ID
749 * @since 4.4.0
750 */
751 public function clear_meta_keys_db_per_post_type_cache_by_post_id( $post_id ) {
752 $post_type = get_post_type( $post_id );
753 if ( $post_type ) {
754 $this->clear_meta_keys_db_cache( $post_type );
755 }
756 }
757
758 /**
759 * Clear the cache of the total fields limit
760 *
761 * @param int|array $meta_id Meta ID
762 * @param int $post_id The post ID
763 * @since 4.4.0
764 */
765 public function clear_meta_keys_db_per_post_type_cache_by_meta( $meta_id, $post_id ) {
766 $post_type = get_post_type( $post_id );
767 if ( $post_type ) {
768 $this->clear_meta_keys_db_cache( $post_type );
769 }
770 }
771
772 /**
773 * Clear the cache of the total fields limit
774 *
775 * @param string $post_type The post type
776 * @since 4.4.0
777 */
778 protected function clear_meta_keys_db_cache( $post_type ) {
779 delete_transient( 'ep_meta_field_keys' );
780 delete_transient( 'ep_meta_field_keys_' . $post_type );
781 }
782
783 /**
784 * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
785 *
786 * @param int $post_id The post ID.
787 * @param string $taxonomy The taxonomy slug.
788 * @return boolean
789 */
790 protected function should_reindex_post( $post_id, $taxonomy ) {
791 /**
792 * Filter to kill post sync
793 *
794 * @hook ep_post_sync_kill
795 * @param {bool} $skip True meanas kill sync for post
796 * @param {int} $object_id ID of post
797 * @param {int} $object_id ID of post
798 * @return {boolean} New value
799 */
800 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
801 return false;
802 }
803
804 $post = get_post( $post_id );
805 if ( ! is_object( $post ) ) {
806 return false;
807 }
808
809 $indexable = Indexables::factory()->get( $this->indexable_slug );
810
811 // Check post status
812 $indexable_post_statuses = $indexable->get_indexable_post_status();
813 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
814 return false;
815 }
816
817 // Only re-index if the taxonomy is indexed for this post
818 $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
819 $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
820 if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
821 return false;
822 }
823
824 // Check post type
825 $indexable_post_types = $indexable->get_indexable_post_types();
826 if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
827 return false;
828 }
829
830 // If we have more items to update than the number set as Content Items per Index Cycle, skip it to avoid a timeout.
831 $single_ids_queued = array_unique( array_keys( $this->sync_queue ) );
832 $has_too_many_queued = count( $single_ids_queued ) > IndexHelper::factory()->get_index_default_per_page();
833
834 return ! $has_too_many_queued;
835 }
836
837 /**
838 * Given a taxonomy, check if the term with most posts is under or above the number set as Content Items per Index Cycle.
839 *
840 * The result will be cached in a transient. Its TTL will depend on the result:
841 * If it is determined we have a term with more posts, cache it for more time.
842 *
843 * @since 4.4.0
844 * @param \WP_Taxonomy $tax The taxonomy object
845 * @return boolean
846 */
847 protected function is_tax_max_count_bigger_than_items_per_cycle( \WP_Taxonomy $tax ) : bool {
848 $transient_name = "ep_term_max_count_{$tax->name}";
849 $cached_max_count = get_transient( $transient_name );
850
851 if ( is_integer( $cached_max_count ) ) {
852 return $cached_max_count > IndexHelper::factory()->get_index_default_per_page();
853 }
854
855 $max_count = get_terms(
856 [
857 'taxonomy' => $tax->name,
858 'orderby' => 'count',
859 'order' => 'DESC',
860 'number' => 1,
861 'count' => true,
862 ]
863 );
864
865 if ( ! is_array( $max_count ) || ! count( $max_count ) || ! $max_count[0] instanceof \WP_Term || ! is_integer( $max_count[0]->count ) ) {
866 set_transient( $transient_name, 0, HOUR_IN_SECONDS );
867 return false;
868 }
869
870 $is_max_count_bigger = $max_count[0]->count > IndexHelper::factory()->get_index_default_per_page();
871
872 set_transient(
873 $transient_name,
874 $max_count[0]->count,
875 $is_max_count_bigger ? DAY_IN_SECONDS : HOUR_IN_SECONDS
876 );
877
878 return $is_max_count_bigger;
879 }
880 }
881