PluginProbe
ElasticPress / 4.6.0
ElasticPress v4.6.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.6.0, at includes/classes/Indexable/Post/SyncManager.php

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