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

944 lines 30.9 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( 'wp_media_attach_action', array( $this, 'action_sync_on_media_attach' ), 999, 2 );
61 add_action( 'delete_post', array( $this, 'action_delete_post' ) );
62 add_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
63 add_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
64 // Called just because we need to know somehow if $delete_all is set before action_queue_meta_sync() runs.
65 add_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ), 10, 5 );
66 add_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
67 add_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );
68
69 add_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
70 add_filter( 'ep_sync_delete_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
71
72 // Conditionally update posts associated with terms
73 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_edit_single_term' ] );
74 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_term_list_screen' ] );
75 add_action( 'set_object_terms', array( $this, 'action_set_object_terms' ), 10, 6 );
76 add_action( 'edited_term', array( $this, 'action_edited_term' ), 10, 3 );
77 add_action( 'deleted_term_relationships', array( $this, 'action_deleted_term_relationships' ), 10, 3 );
78
79 // Clear index settings cache
80 add_action( 'ep_update_index_settings', [ $this, 'clear_index_settings_cache' ] );
81 add_action( 'ep_after_put_mapping', [ $this, 'clear_index_settings_cache' ] );
82 add_action( 'ep_saved_weighting_configuration', [ $this, 'clear_index_settings_cache' ] );
83
84 // Clear distinct meta field per post type cache
85 add_action( 'wp_insert_post', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_post_id' ] );
86 add_action( 'delete_post', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_post_id' ] );
87 add_action( 'updated_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
88 add_action( 'added_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
89 add_action( 'deleted_post_meta', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
90 add_action( 'delete_post_metadata', [ $this, 'clear_meta_keys_db_per_post_type_cache_by_meta' ], 10, 2 );
91
92 // Prevents password protected posts from being indexed
93 add_filter( 'ep_post_sync_kill', [ $this, 'kill_sync_for_password_protected' ], 10, 2 );
94 }
95
96 /**
97 * Un-setup actions and filters (for multisite).
98 *
99 * @since 4.0
100 */
101 public function tear_down() {
102 remove_action( 'wp_insert_post', array( $this, 'action_sync_on_update' ), 999 );
103 remove_action( 'add_attachment', array( $this, 'action_sync_on_update' ), 999 );
104 remove_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999 );
105 remove_action( 'wp_media_attach_action', array( $this, 'action_sync_on_media_attach' ), 999 );
106 remove_action( 'delete_post', array( $this, 'action_delete_post' ) );
107 remove_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ) );
108 remove_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ) );
109 remove_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ) );
110 remove_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ) );
111 remove_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );
112 remove_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
113 remove_filter( 'ep_sync_delete_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
114 remove_filter( 'ep_post_sync_kill', [ $this, 'kill_sync_for_password_protected' ] );
115
116 // Clear index settings cache
117 remove_action( 'ep_update_index_settings', [ $this, 'clear_index_settings_cache' ] );
118 remove_action( 'ep_after_put_mapping', [ $this, 'clear_index_settings_cache' ] );
119 remove_action( 'ep_saved_weighting_configuration', [ $this, 'clear_index_settings_cache' ] );
120 }
121
122 /**
123 * Whether to delete all meta from other posts that is associated with the deleted post.
124 *
125 * @param bool $check Whether to allow metadata deletion of the given type.
126 * @param int $object_id ID of the object metadata is for.
127 * @param string $meta_key Metadata key.
128 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
129 * @param bool $delete_all Whether to delete the matching metadata entries
130 * for all objects, ignoring the specified $object_id
131 * @return bool
132 */
133 public function maybe_delete_meta_for_all( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
134 $this->delete_all_meta = $delete_all;
135 return $check;
136 }
137
138 /**
139 * Filter to allow cron and WP CLI processes to index/delete documents
140 *
141 * @param boolean $bypass The current filtered value
142 * @return boolean Boolean indicating if permission checking should be bypased or not
143 * @since 3.6.0
144 */
145 public function filter_bypass_permission_checks_for_machines( $bypass ) {
146 // Allow index/delete during cron
147 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
148 return true;
149 }
150
151 // Allow index/delete during WP CLI commands
152 if ( defined( 'WP_CLI' ) && WP_CLI ) {
153 return true;
154 }
155
156 return $bypass;
157 }
158
159 /**
160 * When whitelisted meta is updated, queue the post for reindex
161 *
162 * @param int|array $meta_id Meta id.
163 * @param int $object_id Object id.
164 * @param string $meta_key Meta key.
165 * @param string $meta_value Meta value.
166 * @since 2.0
167 */
168 public function action_queue_meta_sync( $meta_id, $object_id, $meta_key, $meta_value ) {
169 if ( $this->kill_sync() ) {
170 return;
171 }
172
173 $indexable = Indexables::factory()->get( $this->indexable_slug );
174
175 /**
176 * Filter to whether skip a sync during autosave, defaults to true
177 *
178 * @hook ep_skip_autosave_sync
179 * @since 4.3.0
180 * @param {bool} $skip True means to disable sync for autosaves
181 * @param {string} $function Function applying filter
182 * @return {boolean} New value
183 */
184 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
185 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
186 // Bypass saving if doing autosave
187 // @codeCoverageIgnoreStart
188 return;
189 // @codeCoverageIgnoreEnd
190 }
191 }
192
193 $post = get_post( $object_id );
194
195 /**
196 * Filter to allow skipping a sync triggered by meta changes
197 *
198 * @hook ep_skip_post_meta_sync
199 * @param {bool} $skip True means kill sync for post
200 * @param {WP_Post} $post The post that's attempting to be synced
201 * @param {int} $meta_id ID of the meta that triggered the sync
202 * @param {string} $meta_key The key of the meta that triggered the sync
203 * @param {string} $meta_value The value of the meta that triggered the sync
204 * @return {boolean} New value
205 */
206 if ( apply_filters( 'ep_skip_post_meta_sync', false, $post, $meta_id, $meta_key, $meta_value ) ) {
207 return;
208 }
209
210 if ( empty( $object_id ) && $this->delete_all_meta ) {
211 add_filter( 'ep_is_integrated_request', '__return_true' );
212
213 $query = new \WP_Query(
214 [
215 'ep_integrate' => true,
216 'meta_key' => $meta_key,
217 'meta_value' => $meta_value,
218 'fields' => 'ids',
219 'post_type' => $indexable->get_indexable_post_types(),
220 ]
221 );
222
223 remove_filter( 'ep_is_integrated_request', '__return_true' );
224
225 if ( $query->have_posts() && $query->elasticsearch_success ) {
226 $posts_to_be_synced = array_filter(
227 $query->posts,
228 function( $object_id ) {
229 return ! apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id );
230 }
231 );
232 if ( ! empty( $posts_to_be_synced ) ) {
233 $indexable->bulk_index( $posts_to_be_synced );
234 }
235 }
236 } else {
237 $indexable_post_statuses = $indexable->get_indexable_post_status();
238 $post_type = get_post_type( $object_id );
239
240 $is_meta_allowed = $indexable->is_meta_allowed( $meta_key, $post );
241 if ( ! $is_meta_allowed ) {
242 return;
243 }
244
245 if ( in_array( $post->post_status, $indexable_post_statuses, true ) ) {
246 $indexable_post_types = $indexable->get_indexable_post_types();
247
248 if ( in_array( $post_type, $indexable_post_types, true ) ) {
249 /**
250 * Filter to kill post sync
251 *
252 * @hook ep_post_sync_kill
253 * @param {bool} $skip True meanas kill sync for post
254 * @param {int} $object_id ID of post
255 * @param {int} $object_id ID of post
256 * @return {boolean} New value
257 */
258 if ( apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id ) ) {
259 return;
260 }
261
262 $this->add_to_queue( $object_id );
263 }
264 }
265 }
266 }
267
268 /**
269 * Delete ES post when WP post is deleted
270 *
271 * @param int $post_id Post id.
272 * @since 0.1.0
273 */
274 public function action_delete_post( $post_id ) {
275 if ( $this->kill_sync() ) {
276 return;
277 }
278
279 /**
280 * Filter whether to skip the permissions check on deleting a post
281 *
282 * @hook ep_sync_delete_permissions_bypass
283 * @param {bool} $bypass True to bypass
284 * @param {int} $post_id ID of post
285 * @return {boolean} New value
286 */
287 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_delete_permissions_bypass', false, $post_id ) ) {
288 return;
289 }
290
291 $indexable = Indexables::factory()->get( $this->indexable_slug );
292 $post_type = get_post_type( $post_id );
293
294 $indexable_post_types = $indexable->get_indexable_post_types();
295
296 if ( ! in_array( $post_type, $indexable_post_types, true ) ) {
297 // If not an indexable post type, skip delete.
298 return;
299 }
300
301 Indexables::factory()->get( $this->indexable_slug )->delete( $post_id, false );
302
303 /**
304 * Make sure to remove this post from the sync queue in case an shutdown happens
305 * before a redirect when a redirect has already been triggered.
306 */
307 if ( isset( $this->sync_queue[ $post_id ] ) ) {
308 unset( $this->sync_queue[ $post_id ] );
309 }
310 }
311
312 /**
313 * Sync ES index with what happened to the post being saved
314 *
315 * @param int $post_id Post id.
316 * @since 0.1.0
317 */
318 public function action_sync_on_update( $post_id ) {
319 if ( $this->kill_sync() ) {
320 return;
321 }
322
323 $indexable = Indexables::factory()->get( $this->indexable_slug );
324 $post_type = get_post_type( $post_id );
325
326 /**
327 * Filter to whether skip a sync during autosave, defaults to true
328 *
329 * @hook ep_skip_autosave_sync
330 * @since 4.3.0
331 * @param {bool} $skip True means to disable sync for autosaves
332 * @param {string} $function Function applying filter
333 * @return {boolean} New value
334 */
335 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
336 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
337 // Bypass saving if doing autosave
338 // @codeCoverageIgnoreStart
339 return;
340 // @codeCoverageIgnoreEnd
341 }
342 }
343
344 /**
345 * Filter whether to skip the permissions check on updating a post
346 *
347 * @hook ep_sync_insert_permissions_bypass
348 * @param {bool} $bypass True to bypass
349 * @param {int} $post_id ID of post
350 * @return {boolean} New value
351 */
352 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_insert_permissions_bypass', false, $post_id ) ) {
353 return;
354 }
355
356 $post = get_post( $post_id );
357
358 $indexable_post_statuses = $indexable->get_indexable_post_status();
359
360 // Our post was published, but is no longer, so let's remove it from the Elasticsearch index.
361 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
362 $this->action_delete_post( $post_id );
363 } else {
364 $indexable_post_types = $indexable->get_indexable_post_types();
365
366 if ( in_array( $post_type, $indexable_post_types, true ) ) {
367 /**
368 * Fire before post is queued for syncing
369 *
370 * @hook ep_sync_on_transition
371 * @param {int} $post_id ID of post
372 */
373 do_action( 'ep_sync_on_transition', $post_id );
374
375 /**
376 * Filter to kill post sync
377 *
378 * @hook ep_post_sync_kill
379 * @param {bool} $skip True means kill sync for post
380 * @param {int} $object_id ID of post
381 * @param {int} $object_id ID of post
382 * @return {boolean} New value
383 */
384 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
385 return;
386 }
387
388 $this->add_to_queue( $post_id );
389 }
390 }
391 }
392
393 /**
394 * Depending on the number of posts associated with the term display an admin notice
395 *
396 * @since 4.4.0
397 * @param array $notices Current ElasticPress admin notices
398 * @return array
399 */
400 public function maybe_display_notice_edit_single_term( $notices ) {
401 global $pagenow, $tag;
402
403 /**
404 * Make sure we're on a term-related page in the admin dashboard.
405 */
406 if ( ! is_admin() || 'term.php' !== $pagenow || ! $tag instanceof \WP_Term ) {
407 return $notices;
408 }
409
410 if ( IndexHelper::factory()->get_index_default_per_page() >= $tag->count ) {
411
412 $child_tags = get_term_children( $tag->term_id, $tag->taxonomy );
413 if ( empty( $child_tags ) ) {
414 return $notices;
415 }
416 foreach ( $child_tags as $child_tag_id ) {
417 $child_tag = get_term( $child_tag_id );
418 if ( ! is_wp_error( $child_tag ) && IndexHelper::factory()->get_index_default_per_page() < $child_tag->count && ! isset( $notices['edited_single_parent_term'] ) ) {
419 $notices['edited_single_parent_term'] = [
420 'html' => sprintf(
421 /* translators: Sync Page URL */
422 __( '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' ),
423 Utils\get_sync_url()
424 ),
425 'type' => 'warning',
426 'dismiss' => true,
427 ];
428 break;
429 }
430 }
431
432 return $notices;
433 }
434 $notices['edited_single_term'] = [
435 'html' => sprintf(
436 /* translators: Sync Page URL */
437 __( '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' ),
438 Utils\get_sync_url()
439 ),
440 'type' => 'warning',
441 'dismiss' => true,
442 ];
443
444 return $notices;
445 }
446
447 /**
448 * Depending on the number of posts display an admin notice in the Dashboard Terms List Screen
449 *
450 * @since 4.4.0
451 * @param array $notices Current ElasticPress admin notices
452 * @return array
453 */
454 public function maybe_display_notice_term_list_screen( $notices ) {
455 global $pagenow, $tax;
456
457 /**
458 * Make sure we're on a term-related page in the admin dashboard.
459 */
460 if ( ! is_admin() || 'edit-tags.php' !== $pagenow || ! $tax instanceof \WP_Taxonomy ) {
461 return $notices;
462 }
463
464 if ( ! $this->is_tax_max_count_bigger_than_items_per_cycle( $tax ) ) {
465 return $notices;
466 }
467
468 $notices['too_many_posts_on_term'] = [
469 'html' => sprintf(
470 /* translators: Sync Page URL */
471 __( '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' ),
472 Utils\get_sync_url()
473 ),
474 'type' => 'warning',
475 'dismiss' => true,
476 ];
477
478 return $notices;
479 }
480
481 /**
482 * When a post's terms are changed, re-index.
483 *
484 * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
485 * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
486 * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
487 * hence why we also have `action_deleted_term_relationships`.
488 *
489 * @see set_object_terms
490 * @param int $post_id Post ID.
491 * @param array $terms An array of object terms.
492 * @param array $tt_ids An array of term taxonomy IDs.
493 * @param string $taxonomy Taxonomy slug.
494 * @param bool $append Whether to append new terms to the old terms.
495 * @param array $old_tt_ids Old array of term taxonomy IDs.
496 * @since 4.0.0
497 */
498 public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
499 if ( $this->kill_sync() ) {
500 return;
501 }
502
503 /**
504 * Filter to whether skip a sync during autosave, defaults to true
505 *
506 * @hook ep_skip_autosave_sync
507 * @since 4.3.0
508 * @param {bool} $skip True means to disable sync for autosaves
509 * @param {string} $function Function applying filter
510 * @return {boolean} New value
511 */
512 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
513 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
514 // Bypass saving if doing autosave
515 // @codeCoverageIgnoreStart
516 return;
517 // @codeCoverageIgnoreEnd
518 }
519 }
520
521 /**
522 * Filter to allow skipping this action in case of custom handling
523 *
524 * @hook ep_skip_action_set_object_terms
525 * @param {bool} $skip True means kill sync for post
526 * @param {int} $post_id ID of post
527 * @param {array} $terms An array of object terms.
528 * @param {array} $tt_ids An array of term taxonomy IDs.
529 * @param {string} $taxonomy Taxonomy slug.
530 * @param {bool} $append Whether to append new terms to the old terms.
531 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
532 * @return {boolean} New value
533 */
534 if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
535 return;
536 }
537
538 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
539 return;
540 }
541
542 /**
543 * Fire before post is queued for syncing
544 *
545 * @since 4.0.0
546 * @hook ep_sync_on_set_object_terms
547 * @param {int} $post_id ID of post
548 * @param {array} $terms An array of object terms.
549 * @param {array} $tt_ids An array of term taxonomy IDs.
550 * @param {string} $taxonomy Taxonomy slug.
551 * @param {bool} $append Whether to append new terms to the old terms.
552 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
553 */
554 do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
555
556 $this->add_to_queue( $post_id );
557 }
558
559 /**
560 * When a term is updated, re-index all posts attached to that term
561 *
562 * @param int $term_id Term id.
563 * @param int $tt_id Term Taxonomy id.
564 * @param string $taxonomy Taxonomy name.
565 * @since 4.0.0
566 */
567 public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
568 global $wpdb;
569
570 if ( $this->kill_sync() ) {
571 return;
572 }
573
574 /**
575 * Filter to whether skip a sync during autosave, defaults to true
576 *
577 * @hook ep_skip_autosave_sync
578 * @since 4.3.0
579 * @param {bool} $skip True means to disable sync for autosaves
580 * @param {string} $function Function applying filter
581 * @return {boolean} New value
582 */
583 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
584 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
585 // Bypass saving if doing autosave
586 // @codeCoverageIgnoreStart
587 return;
588 // @codeCoverageIgnoreEnd
589 }
590 }
591
592 // Find ID of all attached posts (query lifted from wp_delete_term())
593 $object_ids = (array) $wpdb->get_col( // phpcs:disable WordPress.DB.DirectDatabaseQuery
594 $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d", $tt_id )
595 );
596
597 // If the current term is not attached, check if the child terms are attached to the post
598 if ( empty( $object_ids ) ) {
599 $child_terms = get_term_children( $term_id, $taxonomy );
600 if ( ! empty( $child_terms ) ) {
601 $in_id = join( ',', array_fill( 0, count( $child_terms ), '%d' ) );
602 $object_ids = (array) $wpdb->get_col( // phpcs:disable WordPress.DB.DirectDatabaseQuery
603 $wpdb->prepare(
604 "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( {$in_id} )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
605 $child_terms
606 )
607 );
608 }
609 }
610 if ( ! count( $object_ids ) ) {
611 return;
612 }
613
614 // If we have more items to update than the number set as Content Items per Index Cycle, skip it.
615 $should_skip = count( $object_ids ) > IndexHelper::factory()->get_index_default_per_page();
616
617 /**
618 * Filter to allow skipping this action in case of custom handling
619 *
620 * @hook ep_skip_action_edited_term
621 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
622 * @param {int} $term_id Term id.
623 * @param {int} $tt_id Term Taxonomy id.
624 * @param {string} $taxonomy Taxonomy name.
625 * @param {array} $object_ids IDs of the objects attached to the term id.
626 * @return {bool} New value of whether to skip running action_edited_term or not
627 */
628 if ( apply_filters( 'ep_skip_action_edited_term', $should_skip, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
629 return;
630 }
631
632 // Add all of them to the queue
633 foreach ( $object_ids as $post_id ) {
634 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
635 continue;
636 }
637
638 /**
639 * Fire before post is queued for syncing
640 *
641 * @hook ep_sync_on_edited_term
642 * @param {int} $post_id ID of post
643 * @param {int} $term_id ID of the term that was edited
644 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
645 * @param {int} $taxonomy Taxonomy of the term that was edited
646 */
647 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
648
649 $this->add_to_queue( $post_id );
650 }
651 }
652
653 /**
654 * When a term relationship is deleted, re-index all posts attached to that term
655 *
656 * @param int $post_id Post ID.
657 * @param array $tt_ids An array of term taxonomy IDs.
658 * @param string $taxonomy Taxonomy slug.
659 * @since 4.0.0
660 */
661 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
662 if ( $this->kill_sync() ) {
663 return;
664 }
665
666 /**
667 * Filter to whether skip a sync during autosave, defaults to true
668 *
669 * @hook ep_skip_autosave_sync
670 * @since 4.3.0
671 * @param {bool} $skip True means to disable sync for autosaves
672 * @param {string} $function Function applying filter
673 * @return {boolean} New value
674 */
675 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
676 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
677 // Bypass saving if doing autosave
678 // @codeCoverageIgnoreStart
679 return;
680 // @codeCoverageIgnoreEnd
681 }
682 }
683
684 /**
685 * Filter to allow skipping this action in case of custom handling
686 *
687 * @hook ep_skip_action_deleted_term_relationships
688 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
689 * @param {int} $post_id Post ID.
690 * @param {array} $tt_ids An array of term taxonomy IDs.
691 * @param {string} $taxonomy Taxonomy slug.
692 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
693 */
694 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
695 return;
696 }
697
698 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
699 return;
700 }
701
702 /**
703 * Fire before post is queued for syncing
704 *
705 * @hook ep_sync_on_deleted_term_relationships
706 * @since 4.0.0
707 * @param {int} $post_id ID of post
708 * @param {array} $tt_ids An array of term taxonomy IDs.
709 * @param {string} $taxonomy Taxonomy of the term that was edited
710 */
711 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
712
713 $this->add_to_queue( $post_id );
714 }
715
716 /**
717 * Create mapping and network alias when a new blog is created.
718 *
719 * @param WP_Site $blog New site object.
720 */
721 public function action_create_blog_index( $blog ) {
722 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
723 // @codeCoverageIgnoreStart
724 return;
725 // @codeCoverageIgnoreEnd
726 }
727
728 if ( $this->kill_sync() ) {
729 return;
730 }
731
732 $non_global_indexable_objects = Indexables::factory()->get_all( false );
733
734 switch_to_blog( $blog->blog_id );
735
736 foreach ( $non_global_indexable_objects as $indexable ) {
737 $indexable->delete_index();
738 $indexable->put_mapping();
739
740 $index_name = $indexable->get_index_name( $blog->blog_id );
741 $indexable->create_network_alias( [ $index_name ] );
742 }
743
744 restore_current_blog();
745 }
746
747 /**
748 * DEPRECATED. Clear the cache of the total fields limit
749 *
750 * @since 4.4.0
751 */
752 public function clear_total_fields_limit_cache() {
753 _deprecated_function( __METHOD__, '4.7.0', '\ElasticPress\Indexable\Post\SyncManager::clear_index_settings_cache()' );
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 /**
928 * Sync ES index when attached or detached action is called.
929 *
930 * @since 4.7.0
931 * @param string $action Attach/detach action
932 * @param int $attachment_id The attachment ID
933 */
934 public function action_sync_on_media_attach( $action, $attachment_id ) {
935 $indexable = Indexables::factory()->get( $this->indexable_slug );
936 $indexable_post_types = $indexable->get_indexable_post_types();
937
938 if ( ! in_array( 'attachment', $indexable_post_types, true ) ) {
939 return;
940 }
941 $this->action_sync_on_update( $attachment_id );
942 }
943 }
944