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

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

943 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 bypased 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 if ( isset( $this->sync_queue[ $post_id ] ) ) {
307 unset( $this->sync_queue[ $post_id ] );
308 }
309 }
310
311 /**
312 * Sync ES index with what happened to the post being saved
313 *
314 * @param int $post_id Post id.
315 * @since 0.1.0
316 */
317 public function action_sync_on_update( $post_id ) {
318 if ( $this->kill_sync() ) {
319 return;
320 }
321
322 $indexable = Indexables::factory()->get( $this->indexable_slug );
323 $post_type = get_post_type( $post_id );
324
325 /**
326 * Filter to whether skip a sync during autosave, defaults to true
327 *
328 * @hook ep_skip_autosave_sync
329 * @since 4.3.0
330 * @param {bool} $skip True means to disable sync for autosaves
331 * @param {string} $function Function applying filter
332 * @return {boolean} New value
333 */
334 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
335 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
336 // Bypass saving if doing autosave
337 // @codeCoverageIgnoreStart
338 return;
339 // @codeCoverageIgnoreEnd
340 }
341 }
342
343 /**
344 * Filter whether to skip the permissions check on updating a post
345 *
346 * @hook ep_sync_insert_permissions_bypass
347 * @param {bool} $bypass True to bypass
348 * @param {int} $post_id ID of post
349 * @return {boolean} New value
350 */
351 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_insert_permissions_bypass', false, $post_id ) ) {
352 return;
353 }
354
355 $post = get_post( $post_id );
356
357 $indexable_post_statuses = $indexable->get_indexable_post_status();
358
359 // Our post was published, but is no longer, so let's remove it from the Elasticsearch index.
360 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
361 $this->action_delete_post( $post_id );
362 } else {
363 $indexable_post_types = $indexable->get_indexable_post_types();
364
365 if ( in_array( $post_type, $indexable_post_types, true ) ) {
366 /**
367 * Fire before post is queued for syncing
368 *
369 * @hook ep_sync_on_transition
370 * @param {int} $post_id ID of post
371 */
372 do_action( 'ep_sync_on_transition', $post_id );
373
374 /**
375 * Filter to kill post sync
376 *
377 * @hook ep_post_sync_kill
378 * @param {bool} $skip True means kill sync for post
379 * @param {int} $object_id ID of post
380 * @param {int} $object_id ID of post
381 * @return {boolean} New value
382 */
383 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
384 return;
385 }
386
387 $this->add_to_queue( $post_id );
388 }
389 }
390 }
391
392 /**
393 * Depending on the number of posts associated with the term display an admin notice
394 *
395 * @since 4.4.0
396 * @param array $notices Current ElasticPress admin notices
397 * @return array
398 */
399 public function maybe_display_notice_edit_single_term( $notices ) {
400 global $pagenow, $tag;
401
402 /**
403 * Make sure we're on a term-related page in the admin dashboard.
404 */
405 if ( ! is_admin() || 'term.php' !== $pagenow || ! $tag instanceof \WP_Term ) {
406 return $notices;
407 }
408
409 if ( IndexHelper::factory()->get_index_default_per_page() >= $tag->count ) {
410
411 $child_tags = get_term_children( $tag->term_id, $tag->taxonomy );
412 if ( empty( $child_tags ) ) {
413 return $notices;
414 }
415 foreach ( $child_tags as $child_tag_id ) {
416 $child_tag = get_term( $child_tag_id );
417 if ( ! is_wp_error( $child_tag ) && IndexHelper::factory()->get_index_default_per_page() < $child_tag->count && ! isset( $notices['edited_single_parent_term'] ) ) {
418 $notices['edited_single_parent_term'] = [
419 'html' => sprintf(
420 /* translators: Sync Page URL */
421 __( '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' ),
422 Utils\get_sync_url()
423 ),
424 'type' => 'warning',
425 'dismiss' => true,
426 ];
427 break;
428 }
429 }
430
431 return $notices;
432 }
433 $notices['edited_single_term'] = [
434 'html' => sprintf(
435 /* translators: Sync Page URL */
436 __( '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' ),
437 Utils\get_sync_url()
438 ),
439 'type' => 'warning',
440 'dismiss' => true,
441 ];
442
443 return $notices;
444 }
445
446 /**
447 * Depending on the number of posts display an admin notice in the Dashboard Terms List Screen
448 *
449 * @since 4.4.0
450 * @param array $notices Current ElasticPress admin notices
451 * @return array
452 */
453 public function maybe_display_notice_term_list_screen( $notices ) {
454 global $pagenow, $tax;
455
456 /**
457 * Make sure we're on a term-related page in the admin dashboard.
458 */
459 if ( ! is_admin() || 'edit-tags.php' !== $pagenow || ! $tax instanceof \WP_Taxonomy ) {
460 return $notices;
461 }
462
463 if ( ! $this->is_tax_max_count_bigger_than_items_per_cycle( $tax ) ) {
464 return $notices;
465 }
466
467 $notices['too_many_posts_on_term'] = [
468 'html' => sprintf(
469 /* translators: Sync Page URL */
470 __( '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' ),
471 Utils\get_sync_url()
472 ),
473 'type' => 'warning',
474 'dismiss' => true,
475 ];
476
477 return $notices;
478 }
479
480 /**
481 * When a post's terms are changed, re-index.
482 *
483 * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
484 * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
485 * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
486 * hence why we also have `action_deleted_term_relationships`.
487 *
488 * @see set_object_terms
489 * @param int $post_id Post ID.
490 * @param array $terms An array of object terms.
491 * @param array $tt_ids An array of term taxonomy IDs.
492 * @param string $taxonomy Taxonomy slug.
493 * @param bool $append Whether to append new terms to the old terms.
494 * @param array $old_tt_ids Old array of term taxonomy IDs.
495 * @since 4.0.0
496 */
497 public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
498 if ( $this->kill_sync() ) {
499 return;
500 }
501
502 /**
503 * Filter to whether skip a sync during autosave, defaults to true
504 *
505 * @hook ep_skip_autosave_sync
506 * @since 4.3.0
507 * @param {bool} $skip True means to disable sync for autosaves
508 * @param {string} $function Function applying filter
509 * @return {boolean} New value
510 */
511 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
512 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
513 // Bypass saving if doing autosave
514 // @codeCoverageIgnoreStart
515 return;
516 // @codeCoverageIgnoreEnd
517 }
518 }
519
520 /**
521 * Filter to allow skipping this action in case of custom handling
522 *
523 * @hook ep_skip_action_set_object_terms
524 * @param {bool} $skip True means kill sync for post
525 * @param {int} $post_id ID of post
526 * @param {array} $terms An array of object terms.
527 * @param {array} $tt_ids An array of term taxonomy IDs.
528 * @param {string} $taxonomy Taxonomy slug.
529 * @param {bool} $append Whether to append new terms to the old terms.
530 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
531 * @return {boolean} New value
532 */
533 if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
534 return;
535 }
536
537 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
538 return;
539 }
540
541 /**
542 * Fire before post is queued for syncing
543 *
544 * @since 4.0.0
545 * @hook ep_sync_on_set_object_terms
546 * @param {int} $post_id ID of post
547 * @param {array} $terms An array of object terms.
548 * @param {array} $tt_ids An array of term taxonomy IDs.
549 * @param {string} $taxonomy Taxonomy slug.
550 * @param {bool} $append Whether to append new terms to the old terms.
551 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
552 */
553 do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
554
555 $this->add_to_queue( $post_id );
556 }
557
558 /**
559 * When a term is updated, re-index all posts attached to that term
560 *
561 * @param int $term_id Term id.
562 * @param int $tt_id Term Taxonomy id.
563 * @param string $taxonomy Taxonomy name.
564 * @since 4.0.0
565 */
566 public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
567 global $wpdb;
568
569 if ( $this->kill_sync() ) {
570 return;
571 }
572
573 /**
574 * Filter to whether skip a sync during autosave, defaults to true
575 *
576 * @hook ep_skip_autosave_sync
577 * @since 4.3.0
578 * @param {bool} $skip True means to disable sync for autosaves
579 * @param {string} $function Function applying filter
580 * @return {boolean} New value
581 */
582 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
583 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
584 // Bypass saving if doing autosave
585 // @codeCoverageIgnoreStart
586 return;
587 // @codeCoverageIgnoreEnd
588 }
589 }
590
591 // Find ID of all attached posts (query lifted from wp_delete_term())
592 $object_ids = (array) $wpdb->get_col( // phpcs:disable WordPress.DB.DirectDatabaseQuery
593 $wpdb->prepare( "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d", $tt_id )
594 );
595
596 // If the current term is not attached, check if the child terms are attached to the post
597 if ( empty( $object_ids ) ) {
598 $child_terms = get_term_children( $term_id, $taxonomy );
599 if ( ! empty( $child_terms ) ) {
600 $in_id = join( ',', array_fill( 0, count( $child_terms ), '%d' ) );
601 $object_ids = (array) $wpdb->get_col( // phpcs:disable WordPress.DB.DirectDatabaseQuery
602 $wpdb->prepare(
603 "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( {$in_id} )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
604 $child_terms
605 )
606 );
607 }
608 }
609 if ( ! count( $object_ids ) ) {
610 return;
611 }
612
613 // If we have more items to update than the number set as Content Items per Index Cycle, skip it.
614 $should_skip = count( $object_ids ) > IndexHelper::factory()->get_index_default_per_page();
615
616 /**
617 * Filter to allow skipping this action in case of custom handling
618 *
619 * @hook ep_skip_action_edited_term
620 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
621 * @param {int} $term_id Term id.
622 * @param {int} $tt_id Term Taxonomy id.
623 * @param {string} $taxonomy Taxonomy name.
624 * @param {array} $object_ids IDs of the objects attached to the term id.
625 * @return {bool} New value of whether to skip running action_edited_term or not
626 */
627 if ( apply_filters( 'ep_skip_action_edited_term', $should_skip, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
628 return;
629 }
630
631 // Add all of them to the queue
632 foreach ( $object_ids as $post_id ) {
633 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
634 continue;
635 }
636
637 /**
638 * Fire before post is queued for syncing
639 *
640 * @hook ep_sync_on_edited_term
641 * @param {int} $post_id ID of post
642 * @param {int} $term_id ID of the term that was edited
643 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
644 * @param {int} $taxonomy Taxonomy of the term that was edited
645 */
646 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
647
648 $this->add_to_queue( $post_id );
649 }
650 }
651
652 /**
653 * When a term relationship is deleted, re-index all posts attached to that term
654 *
655 * @param int $post_id Post ID.
656 * @param array $tt_ids An array of term taxonomy IDs.
657 * @param string $taxonomy Taxonomy slug.
658 * @since 4.0.0
659 */
660 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
661 if ( $this->kill_sync() ) {
662 return;
663 }
664
665 /**
666 * Filter to whether skip a sync during autosave, defaults to true
667 *
668 * @hook ep_skip_autosave_sync
669 * @since 4.3.0
670 * @param {bool} $skip True means to disable sync for autosaves
671 * @param {string} $function Function applying filter
672 * @return {boolean} New value
673 */
674 if ( apply_filters( 'ep_skip_autosave_sync', true, __FUNCTION__ ) ) {
675 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
676 // Bypass saving if doing autosave
677 // @codeCoverageIgnoreStart
678 return;
679 // @codeCoverageIgnoreEnd
680 }
681 }
682
683 /**
684 * Filter to allow skipping this action in case of custom handling
685 *
686 * @hook ep_skip_action_deleted_term_relationships
687 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
688 * @param {int} $post_id Post ID.
689 * @param {array} $tt_ids An array of term taxonomy IDs.
690 * @param {string} $taxonomy Taxonomy slug.
691 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
692 */
693 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
694 return;
695 }
696
697 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
698 return;
699 }
700
701 /**
702 * Fire before post is queued for syncing
703 *
704 * @hook ep_sync_on_deleted_term_relationships
705 * @since 4.0.0
706 * @param {int} $post_id ID of post
707 * @param {array} $tt_ids An array of term taxonomy IDs.
708 * @param {string} $taxonomy Taxonomy of the term that was edited
709 */
710 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
711
712 $this->add_to_queue( $post_id );
713 }
714
715 /**
716 * Create mapping and network alias when a new blog is created.
717 *
718 * @param WP_Site $blog New site object.
719 */
720 public function action_create_blog_index( $blog ) {
721 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
722 // @codeCoverageIgnoreStart
723 return;
724 // @codeCoverageIgnoreEnd
725 }
726
727 if ( $this->kill_sync() ) {
728 return;
729 }
730
731 $non_global_indexable_objects = Indexables::factory()->get_all( false );
732
733 switch_to_blog( $blog->blog_id );
734
735 foreach ( $non_global_indexable_objects as $indexable ) {
736 $indexable->delete_index();
737 $indexable->put_mapping();
738
739 $index_name = $indexable->get_index_name( $blog->blog_id );
740 $indexable->create_network_alias( [ $index_name ] );
741 }
742
743 restore_current_blog();
744 }
745
746 /**
747 * DEPRECATED. Clear the cache of the total fields limit
748 *
749 * @since 4.4.0
750 */
751 public function clear_total_fields_limit_cache() {
752 _deprecated_function( __METHOD__, '4.7.0', '\ElasticPress\Indexable\Post\SyncManager::clear_index_settings_cache()' );
753 }
754
755 /**
756 * Clear the cache of the total fields limit
757 *
758 * @param int $post_id The post ID
759 * @since 4.4.0
760 */
761 public function clear_meta_keys_db_per_post_type_cache_by_post_id( $post_id ) {
762 $post_type = get_post_type( $post_id );
763 if ( $post_type ) {
764 $this->clear_meta_keys_db_cache( $post_type );
765 }
766 }
767
768 /**
769 * Clear the cache of the total fields limit
770 *
771 * @param int|array $meta_id Meta ID
772 * @param int $post_id The post ID
773 * @since 4.4.0
774 */
775 public function clear_meta_keys_db_per_post_type_cache_by_meta( $meta_id, $post_id ) {
776 $post_type = get_post_type( $post_id );
777 if ( $post_type ) {
778 $this->clear_meta_keys_db_cache( $post_type );
779 }
780 }
781
782 /**
783 * Clear the cache of the total fields limit
784 *
785 * @param string $post_type The post type
786 * @since 4.4.0
787 */
788 protected function clear_meta_keys_db_cache( $post_type ) {
789 delete_transient( 'ep_meta_field_keys' );
790 delete_transient( 'ep_meta_field_keys_' . $post_type );
791 }
792
793 /**
794 * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
795 *
796 * @param int $post_id The post ID.
797 * @param string $taxonomy The taxonomy slug.
798 * @return boolean
799 */
800 protected function should_reindex_post( $post_id, $taxonomy ) {
801 /**
802 * Filter to kill post sync
803 *
804 * @hook ep_post_sync_kill
805 * @param {bool} $skip True meanas kill sync for post
806 * @param {int} $object_id ID of post
807 * @param {int} $object_id ID of post
808 * @return {boolean} New value
809 */
810 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
811 return false;
812 }
813
814 $post = get_post( $post_id );
815 if ( ! is_object( $post ) ) {
816 return false;
817 }
818
819 $indexable = Indexables::factory()->get( $this->indexable_slug );
820
821 // Check post status
822 $indexable_post_statuses = $indexable->get_indexable_post_status();
823 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
824 return false;
825 }
826
827 // Only re-index if the taxonomy is indexed for this post
828 $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
829 $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
830 if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
831 return false;
832 }
833
834 // Check post type
835 $indexable_post_types = $indexable->get_indexable_post_types();
836 if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
837 return false;
838 }
839
840 // If we have more items to update than the number set as Content Items per Index Cycle, skip it to avoid a timeout.
841 $single_ids_queued = array_unique( array_keys( $this->sync_queue ) );
842 $has_too_many_queued = count( $single_ids_queued ) > IndexHelper::factory()->get_index_default_per_page();
843
844 return ! $has_too_many_queued;
845 }
846
847 /**
848 * Given a taxonomy, check if the term with most posts is under or above the number set as Content Items per Index Cycle.
849 *
850 * The result will be cached in a transient. Its TTL will depend on the result:
851 * If it is determined we have a term with more posts, cache it for more time.
852 *
853 * @since 4.4.0
854 * @param \WP_Taxonomy $tax The taxonomy object
855 * @return boolean
856 */
857 protected function is_tax_max_count_bigger_than_items_per_cycle( \WP_Taxonomy $tax ) : bool {
858 $transient_name = "ep_term_max_count_{$tax->name}";
859 $cached_max_count = get_transient( $transient_name );
860
861 if ( is_integer( $cached_max_count ) ) {
862 return $cached_max_count > IndexHelper::factory()->get_index_default_per_page();
863 }
864
865 $max_count = get_terms(
866 [
867 'taxonomy' => $tax->name,
868 'orderby' => 'count',
869 'order' => 'DESC',
870 'number' => 1,
871 'count' => true,
872 ]
873 );
874
875 if ( ! is_array( $max_count ) || ! count( $max_count ) || ! $max_count[0] instanceof \WP_Term || ! is_integer( $max_count[0]->count ) ) {
876 set_transient( $transient_name, 0, HOUR_IN_SECONDS );
877 return false;
878 }
879
880 $is_max_count_bigger = $max_count[0]->count > IndexHelper::factory()->get_index_default_per_page();
881
882 set_transient(
883 $transient_name,
884 $max_count[0]->count,
885 $is_max_count_bigger ? DAY_IN_SECONDS : HOUR_IN_SECONDS
886 );
887
888 return $is_max_count_bigger;
889 }
890
891 /**
892 * Prevent a password protected post from being indexed.
893 *
894 * @since 4.6.0
895 * @param bool $skip Whether should skip or not before checking for a password
896 * @param int $object_id The Post ID
897 * @return bool New value of $skip
898 */
899 public function kill_sync_for_password_protected( $skip, $object_id ) {
900 /**
901 * Short-circuits the process of checking if a post should be indexed or not depending on its password.
902 *
903 * Returning a non-null value will effectively short-circuit the function.
904 *
905 * @since 4.6.0
906 * @hook ep_pre_kill_sync_for_password_protected
907 * @param {null} $new_skip Whether should skip or not before checking for a password
908 * @param {bool} $current_skip Current value
909 * @param {int} $object_id The Post ID
910 * @return {null|bool} New value of $skip or `null` to keep default behavior.
911 */
912 $skip_filter = apply_filters( 'ep_pre_kill_sync_for_password_protected', null, $skip, $object_id );
913 if ( ! is_null( $skip_filter ) ) {
914 return $skip_filter;
915 }
916
917 if ( $skip ) {
918 return $skip;
919 }
920
921 $post = get_post( $object_id );
922
923 return ! empty( $post->post_password );
924 }
925
926 /**
927 * Sync ES index when attached or detached action is called.
928 *
929 * @since 4.7.0
930 * @param string $action Attach/detach action
931 * @param int $attachment_id The attachment ID
932 */
933 public function action_sync_on_media_attach( $action, $attachment_id ) {
934 $indexable = Indexables::factory()->get( $this->indexable_slug );
935 $indexable_post_types = $indexable->get_indexable_post_types();
936
937 if ( ! in_array( 'attachment', $indexable_post_types, true ) ) {
938 return;
939 }
940 $this->action_sync_on_update( $attachment_id );
941 }
942 }
943