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

597 lines 18.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
15 if ( ! defined( 'ABSPATH' ) ) {
16 // @codeCoverageIgnoreStart
17 exit; // Exit if accessed directly.
18 // @codeCoverageIgnoreEnd
19 }
20
21 /**
22 * Sync manager class
23 */
24 class SyncManager extends SyncManagerAbstract {
25
26 /**
27 * Indexable slug
28 *
29 * @since 3.0
30 * @var string
31 */
32 public $indexable_slug = 'post';
33
34 /**
35 * Delete all post meta from other posts associated with a deleted post. Useful for attachments.
36 *
37 * @var bool
38 */
39 public $delete_all_meta = false;
40
41 /**
42 * Setup actions and filters
43 *
44 * @since 0.1.2
45 */
46 public function setup() {
47 if ( ! Elasticsearch::factory()->get_elasticsearch_version() ) {
48 return;
49 }
50
51 if ( ! $this->can_index_site() ) {
52 return;
53 }
54
55 add_action( 'wp_insert_post', array( $this, 'action_sync_on_update' ), 999, 3 );
56 add_action( 'add_attachment', array( $this, 'action_sync_on_update' ), 999, 3 );
57 add_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999, 3 );
58 add_action( 'delete_post', array( $this, 'action_delete_post' ) );
59 add_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
60 add_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
61 // Called just because we need to know somehow if $delete_all is set before action_queue_meta_sync() runs.
62 add_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ), 10, 5 );
63 add_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 );
64 add_action( 'set_object_terms', array( $this, 'action_set_object_terms' ), 10, 6 );
65 add_action( 'edited_term', array( $this, 'action_edited_term' ), 10, 3 );
66 add_action( 'deleted_term_relationships', array( $this, 'action_deleted_term_relationships' ), 10, 3 );
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
73 /**
74 * Un-setup actions and filters (for multisite).
75 *
76 * @since 4.0
77 */
78 public function tear_down() {
79 remove_action( 'wp_insert_post', array( $this, 'action_sync_on_update' ), 999 );
80 remove_action( 'add_attachment', array( $this, 'action_sync_on_update' ), 999 );
81 remove_action( 'edit_attachment', array( $this, 'action_sync_on_update' ), 999 );
82 remove_action( 'delete_post', array( $this, 'action_delete_post' ) );
83 remove_action( 'updated_post_meta', array( $this, 'action_queue_meta_sync' ) );
84 remove_action( 'added_post_meta', array( $this, 'action_queue_meta_sync' ) );
85 remove_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ) );
86 remove_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ) );
87 remove_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) );
88 remove_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
89 remove_filter( 'ep_sync_delete_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) );
90 }
91
92 /**
93 * Whether to delete all meta from other posts that is associated with the deleted post.
94 *
95 * @param bool $check Whether to allow metadata deletion of the given type.
96 * @param int $object_id ID of the object metadata is for.
97 * @param string $meta_key Metadata key.
98 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
99 * @param bool $delete_all Whether to delete the matching metadata entries
100 * for all objects, ignoring the specified $object_id
101 * @return bool
102 */
103 public function maybe_delete_meta_for_all( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
104 $this->delete_all_meta = $delete_all;
105 return $check;
106 }
107
108 /**
109 * Filter to allow cron and WP CLI processes to index/delete documents
110 *
111 * @param boolean $bypass The current filtered value
112 * @return boolean Boolean indicating if permission checking should be bypased or not
113 * @since 3.6.0
114 */
115 public function filter_bypass_permission_checks_for_machines( $bypass ) {
116 // Allow index/delete during cron
117 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
118 return true;
119 }
120
121 // Allow index/delete during WP CLI commands
122 if ( defined( 'WP_CLI' ) && WP_CLI ) {
123 return true;
124 }
125
126 return $bypass;
127 }
128
129 /**
130 * When whitelisted meta is updated, queue the post for reindex
131 *
132 * @param int|array $meta_id Meta id.
133 * @param int $object_id Object id.
134 * @param string $meta_key Meta key.
135 * @param string $meta_value Meta value.
136 * @since 2.0
137 */
138 public function action_queue_meta_sync( $meta_id, $object_id, $meta_key, $meta_value ) {
139 if ( $this->kill_sync() ) {
140 return;
141 }
142
143 $indexable = Indexables::factory()->get( $this->indexable_slug );
144
145 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
146 // Bypass saving if doing autosave
147 // @codeCoverageIgnoreStart
148 return;
149 // @codeCoverageIgnoreEnd
150 }
151
152 $post = get_post( $object_id );
153
154 /**
155 * Filter to allow skipping a sync triggered by meta changes
156 *
157 * @hook ep_skip_post_meta_sync
158 * @param {bool} $skip True means kill sync for post
159 * @param {WP_Post} $post The post that's attempting to be synced
160 * @param {int} $meta_id ID of the meta that triggered the sync
161 * @param {string} $meta_key The key of the meta that triggered the sync
162 * @param {string} $meta_value The value of the meta that triggered the sync
163 * @return {boolean} New value
164 */
165 if ( apply_filters( 'ep_skip_post_meta_sync', false, $post, $meta_id, $meta_key, $meta_value ) ) {
166 return;
167 }
168
169 if ( empty( $object_id ) && $this->delete_all_meta ) {
170 add_filter( 'ep_is_integrated_request', '__return_true' );
171
172 $query = new \WP_Query(
173 [
174 'ep_integrate' => true,
175 'meta_key' => $meta_key,
176 'meta_value' => $meta_value,
177 'fields' => 'ids',
178 ]
179 );
180
181 remove_filter( 'ep_is_integrated_request', '__return_true' );
182
183 if ( $query->have_posts() && $query->elasticsearch_success ) {
184 $posts_to_be_synced = array_filter(
185 $query->posts,
186 function( $object_id ) {
187 return ! apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id );
188 }
189 );
190 if ( ! empty( $posts_to_be_synced ) ) {
191 $indexable->bulk_index( $posts_to_be_synced );
192 }
193 }
194 } else {
195 $indexable_post_statuses = $indexable->get_indexable_post_status();
196 $post_type = get_post_type( $object_id );
197
198 $allowed_meta_to_be_indexed = $indexable->prepare_meta( $post );
199 if ( ! in_array( $meta_key, array_keys( $allowed_meta_to_be_indexed ), true ) ) {
200 return;
201 }
202
203 if ( in_array( $post->post_status, $indexable_post_statuses, true ) ) {
204 $indexable_post_types = $indexable->get_indexable_post_types();
205
206 if ( in_array( $post_type, $indexable_post_types, true ) ) {
207 /**
208 * Filter to kill post sync
209 *
210 * @hook ep_post_sync_kill
211 * @param {bool} $skip True meanas kill sync for post
212 * @param {int} $object_id ID of post
213 * @param {int} $object_id ID of post
214 * @return {boolean} New value
215 */
216 if ( apply_filters( 'ep_post_sync_kill', false, $object_id, $object_id ) ) {
217 return;
218 }
219
220 $this->add_to_queue( $object_id );
221 }
222 }
223 }
224 }
225
226 /**
227 * Delete ES post when WP post is deleted
228 *
229 * @param int $post_id Post id.
230 * @since 0.1.0
231 */
232 public function action_delete_post( $post_id ) {
233 if ( $this->kill_sync() ) {
234 return;
235 }
236
237 /**
238 * Filter whether to skip the permissions check on deleting a post
239 *
240 * @hook ep_sync_delete_permissions_bypass
241 * @param {bool} $bypass True to bypass
242 * @param {int} $post_id ID of post
243 * @return {boolean} New value
244 */
245 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_delete_permissions_bypass', false, $post_id ) ) {
246 return;
247 }
248
249 $indexable = Indexables::factory()->get( $this->indexable_slug );
250 $post_type = get_post_type( $post_id );
251
252 $indexable_post_types = $indexable->get_indexable_post_types();
253
254 if ( ! in_array( $post_type, $indexable_post_types, true ) ) {
255 // If not an indexable post type, skip delete.
256 return;
257 }
258
259 Indexables::factory()->get( $this->indexable_slug )->delete( $post_id, false );
260
261 /**
262 * Make sure to remove this post from the sync queue in case an shutdown happens
263 * before a redirect when a redirect has already been triggered.
264 */
265 if ( isset( $this->sync_queue[ $post_id ] ) ) {
266 unset( $this->sync_queue[ $post_id ] );
267 }
268 }
269
270 /**
271 * Sync ES index with what happened to the post being saved
272 *
273 * @param int $post_id Post id.
274 * @since 0.1.0
275 */
276 public function action_sync_on_update( $post_id ) {
277 if ( $this->kill_sync() ) {
278 return;
279 }
280
281 $indexable = Indexables::factory()->get( $this->indexable_slug );
282 $post_type = get_post_type( $post_id );
283
284 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
285 // Bypass saving if doing autosave
286 // @codeCoverageIgnoreStart
287 return;
288 // @codeCoverageIgnoreEnd
289 }
290
291 /**
292 * Filter whether to skip the permissions check on updating a post
293 *
294 * @hook ep_sync_insert_permissions_bypass
295 * @param {bool} $bypass True to bypass
296 * @param {int} $post_id ID of post
297 * @return {boolean} New value
298 */
299 if ( ! current_user_can( 'edit_post', $post_id ) && ! apply_filters( 'ep_sync_insert_permissions_bypass', false, $post_id ) ) {
300 return;
301 }
302
303 $post = get_post( $post_id );
304
305 $indexable_post_statuses = $indexable->get_indexable_post_status();
306
307 // Our post was published, but is no longer, so let's remove it from the Elasticsearch index.
308 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
309 $this->action_delete_post( $post_id );
310 } else {
311 $indexable_post_types = $indexable->get_indexable_post_types();
312
313 if ( in_array( $post_type, $indexable_post_types, true ) ) {
314 /**
315 * Fire before post is queued for syncing
316 *
317 * @hook ep_sync_on_transition
318 * @param {int} $post_id ID of post
319 */
320 do_action( 'ep_sync_on_transition', $post_id );
321
322 /**
323 * Filter to kill post sync
324 *
325 * @hook ep_post_sync_kill
326 * @param {bool} $skip True means kill sync for post
327 * @param {int} $object_id ID of post
328 * @param {int} $object_id ID of post
329 * @return {boolean} New value
330 */
331 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
332 return;
333 }
334
335 $this->add_to_queue( $post_id );
336 }
337 }
338 }
339
340 /**
341 * When a post's terms are changed, re-index.
342 *
343 * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects
344 * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate
345 * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories),
346 * hence why we also have `action_deleted_term_relationships`.
347 *
348 * @see set_object_terms
349 * @param int $post_id Post ID.
350 * @param array $terms An array of object terms.
351 * @param array $tt_ids An array of term taxonomy IDs.
352 * @param string $taxonomy Taxonomy slug.
353 * @param bool $append Whether to append new terms to the old terms.
354 * @param array $old_tt_ids Old array of term taxonomy IDs.
355 * @since 4.0.0
356 */
357 public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
358 if ( $this->kill_sync() ) {
359 return;
360 }
361
362 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
363 // Bypass saving if doing autosave
364 return;
365 }
366
367 /**
368 * Filter to allow skipping this action in case of custom handling
369 *
370 * @hook ep_skip_action_set_object_terms
371 * @param {bool} $skip True means kill sync for post
372 * @param {int} $post_id ID of post
373 * @param {array} $terms An array of object terms.
374 * @param {array} $tt_ids An array of term taxonomy IDs.
375 * @param {string} $taxonomy Taxonomy slug.
376 * @param {bool} $append Whether to append new terms to the old terms.
377 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
378 * @return {boolean} New value
379 */
380 if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) {
381 return;
382 }
383
384 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
385 return;
386 }
387
388 /**
389 * Fire before post is queued for syncing
390 *
391 * @since 4.0.0
392 * @hook ep_sync_on_set_object_terms
393 * @param {int} $post_id ID of post
394 * @param {array} $terms An array of object terms.
395 * @param {array} $tt_ids An array of term taxonomy IDs.
396 * @param {string} $taxonomy Taxonomy slug.
397 * @param {bool} $append Whether to append new terms to the old terms.
398 * @param {array} $old_tt_ids Old array of term taxonomy IDs.
399 */
400 do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
401
402 $this->add_to_queue( $post_id );
403 }
404
405 /**
406 * When a term is updated, re-index all posts attached to that term
407 *
408 * @param int $term_id Term id.
409 * @param int $tt_id Term Taxonomy id.
410 * @param string $taxonomy Taxonomy name.
411 * @since 4.0.0
412 */
413 public function action_edited_term( $term_id, $tt_id, $taxonomy ) {
414 global $wpdb;
415
416 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
417 // Bypass saving if doing autosave
418 return;
419 }
420
421 // Find ID of all attached posts (query lifted from wp_delete_term())
422 $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) );
423
424 if ( ! count( $object_ids ) ) {
425 return;
426 }
427
428 /**
429 * Filter to allow skipping this action in case of custom handling
430 *
431 * @hook ep_skip_action_edited_term
432 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
433 * @param {int} $term_id Term id.
434 * @param {int} $tt_id Term Taxonomy id.
435 * @param {string} $taxonomy Taxonomy name.
436 * @param {array} $object_ids IDs of the objects attached to the term id.
437 * @return {bool} New value of whether to skip running action_edited_term or not
438 */
439 if ( apply_filters( 'ep_skip_action_edited_term', false, $term_id, $tt_id, $taxonomy, $object_ids ) ) {
440 return;
441 }
442
443 $indexable = Indexables::factory()->get( $this->indexable_slug );
444
445 // Add all of them to the queue
446 foreach ( $object_ids as $post_id ) {
447 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
448 continue;
449 }
450
451 /**
452 * Fire before post is queued for syncing
453 *
454 * @hook ep_sync_on_edited_term
455 * @param {int} $post_id ID of post
456 * @param {int} $term_id ID of the term that was edited
457 * @param {int} $tt_id Taxonomy Term ID of the term that was edited
458 * @param {int} $taxonomy Taxonomy of the term that was edited
459 */
460 do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy );
461
462 $this->add_to_queue( $post_id );
463 }
464 }
465
466 /**
467 * When a term relationship is deleted, re-index all posts attached to that term
468 *
469 * @param int $post_id Post ID.
470 * @param array $tt_ids An array of term taxonomy IDs.
471 * @param string $taxonomy Taxonomy slug.
472 * @since 4.0.0
473 */
474 public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) {
475 if ( $this->kill_sync() ) {
476 return;
477 }
478
479 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
480 // Bypass saving if doing autosave
481 return;
482 }
483
484 /**
485 * Filter to allow skipping this action in case of custom handling
486 *
487 * @hook ep_skip_action_deleted_term_relationships
488 * @param {bool} $skip Current value of whether to skip running action_edited_term or not
489 * @param {int} $post_id Post ID.
490 * @param {array} $tt_ids An array of term taxonomy IDs.
491 * @param {string} $taxonomy Taxonomy slug.
492 * @return {bool} New value of whether to skip running action_deleted_term_relationships or not
493 */
494 if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) {
495 return;
496 }
497
498 if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) {
499 return;
500 }
501
502 /**
503 * Fire before post is queued for syncing
504 *
505 * @hook ep_sync_on_deleted_term_relationships
506 * @since 4.0.0
507 * @param {int} $post_id ID of post
508 * @param {array} $tt_ids An array of term taxonomy IDs.
509 * @param {string} $taxonomy Taxonomy of the term that was edited
510 */
511 do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy );
512
513 $this->add_to_queue( $post_id );
514 }
515
516 /**
517 * Create mapping and network alias when a new blog is created.
518 *
519 * @param WP_Site $blog New site object.
520 */
521 public function action_create_blog_index( $blog ) {
522 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
523 // @codeCoverageIgnoreStart
524 return;
525 // @codeCoverageIgnoreEnd
526 }
527
528 if ( $this->kill_sync() ) {
529 return;
530 }
531
532 $non_global_indexable_objects = Indexables::factory()->get_all( false );
533
534 switch_to_blog( $blog->blog_id );
535
536 foreach ( $non_global_indexable_objects as $indexable ) {
537 $indexable->delete_index();
538 $indexable->put_mapping();
539
540 $index_name = $indexable->get_index_name( $blog->blog_id );
541 $indexable->create_network_alias( [ $index_name ] );
542 }
543
544 restore_current_blog();
545 }
546
547 /**
548 * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not.
549 *
550 * @param int $post_id The post ID.
551 * @param string $taxonomy The taxonomy slug.
552 * @return boolean
553 */
554 protected function should_reindex_post( $post_id, $taxonomy ) {
555 /**
556 * Filter to kill post sync
557 *
558 * @hook ep_post_sync_kill
559 * @param {bool} $skip True meanas kill sync for post
560 * @param {int} $object_id ID of post
561 * @param {int} $object_id ID of post
562 * @return {boolean} New value
563 */
564 if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) {
565 return false;
566 }
567
568 $post = get_post( $post_id );
569 if ( ! is_object( $post ) ) {
570 return false;
571 }
572
573 $indexable = Indexables::factory()->get( $this->indexable_slug );
574
575 // Check post status
576 $indexable_post_statuses = $indexable->get_indexable_post_status();
577 if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) {
578 return false;
579 }
580
581 // Only re-index if the taxonomy is indexed for this post
582 $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post );
583 $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' );
584 if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) {
585 return false;
586 }
587
588 // Check post type
589 $indexable_post_types = $indexable->get_indexable_post_types();
590 if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) {
591 return false;
592 }
593
594 return true;
595 }
596 }
597