PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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.3.5, at includes/classes/Indexable/Post/SyncManager.php

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