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

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

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