From 28e7d9c5b4051efdeaab6e82bc87fa5a210c3779 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 21 Feb 2022 13:56:58 -0300 Subject: [PATCH 1/2] Update posts on term changes --- includes/classes/Indexable/Post/Post.php | 49 ++- .../classes/Indexable/Post/SyncManager.php | 286 ++++++++++++++++++ tests/php/indexables/TestPost.php | 70 +++++ 3 files changed, 390 insertions(+), 15 deletions(-) diff --git a/includes/classes/Indexable/Post/Post.php b/includes/classes/Indexable/Post/Post.php index 94e134bf6f..cc43d53ce1 100644 --- a/includes/classes/Indexable/Post/Post.php +++ b/includes/classes/Indexable/Post/Post.php @@ -559,13 +559,13 @@ public function prepare_date_terms( $date_to_prepare ) { } /** - * Prepare terms to send to ES. + * Get an array of taxonomies that are indexable for the given post * + * @since 4.0.0 * @param WP_Post $post Post object - * @since 0.1.0 - * @return array + * @return array Array of WP_Taxonomy objects that should be indexed */ - private function prepare_terms( $post ) { + public function get_indexable_post_taxonomies( $post ) { $taxonomies = get_object_taxonomies( $post->post_type, 'objects' ); $selected_taxonomies = []; @@ -583,7 +583,36 @@ private function prepare_terms( $post ) { * @param {WP_Post} Post object * @return {array} New taxonomies */ - $selected_taxonomies = apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post ); + $selected_taxonomies = (array) apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post ); + + // Important we validate here to ensure there are no invalid taxonomy values returned from the filter, as just one would cause wp_get_object_terms() to fail. + $validated_taxonomies = []; + foreach ( $selected_taxonomies as $selected_taxonomy ) { + // If we get a taxonomy name, we need to convert it to taxonomy object + if ( ! is_object( $selected_taxonomy ) && taxonomy_exists( (string) $selected_taxonomy ) ) { + $selected_taxonomy = get_taxonomy( $selected_taxonomy ); + } + + // We check if the $taxonomy object has a valid name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7 + if ( ! is_a( $selected_taxonomy, '\WP_Taxonomy' ) || ! property_exists( $selected_taxonomy, 'name' ) || ! taxonomy_exists( $selected_taxonomy->name ) ) { + continue; + } + + $validated_taxonomies[] = $selected_taxonomy; + } + + return $validated_taxonomies; + } + + /** + * Prepare terms to send to ES. + * + * @param WP_Post $post Post object + * @since 0.1.0 + * @return array + */ + private function prepare_terms( $post ) { + $selected_taxonomies = $this->get_indexable_post_taxonomies( $post ); if ( empty( $selected_taxonomies ) ) { return []; @@ -601,16 +630,6 @@ private function prepare_terms( $post ) { $allow_hierarchy = apply_filters( 'ep_sync_terms_allow_hierarchy', true ); foreach ( $selected_taxonomies as $taxonomy ) { - // If we get a taxonomy name, we need to convert it to taxonomy object - if ( ! is_object( $taxonomy ) && taxonomy_exists( (string) $taxonomy ) ) { - $taxonomy = get_taxonomy( $taxonomy ); - } - - // We check if the $taxonomy object as name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7 - if ( ! is_a( $taxonomy, '\WP_Taxonomy' ) || ! property_exists( $taxonomy, 'name' ) ) { - continue; - } - $object_terms = get_the_terms( $post->ID, $taxonomy->name ); if ( ! $object_terms || is_wp_error( $object_terms ) ) { diff --git a/includes/classes/Indexable/Post/SyncManager.php b/includes/classes/Indexable/Post/SyncManager.php index 49206b4b2d..7777d35c2e 100644 --- a/includes/classes/Indexable/Post/SyncManager.php +++ b/includes/classes/Indexable/Post/SyncManager.php @@ -61,6 +61,9 @@ public function setup() { // Called just because we need to know somehow if $delete_all is set before action_queue_meta_sync() runs. add_filter( 'delete_post_metadata', array( $this, 'maybe_delete_meta_for_all' ), 10, 5 ); add_action( 'deleted_post_meta', array( $this, 'action_queue_meta_sync' ), 10, 4 ); + add_action( 'set_object_terms', array( $this, 'action_set_object_terms' ), 10, 6 ); + add_action( 'edited_term', array( $this, 'action_edited_term' ), 10, 3 ); + add_action( 'deleted_term_relationships', array( $this, 'action_deleted_term_relationships' ), 10, 3 ); add_action( 'wp_initialize_site', array( $this, 'action_create_blog_index' ) ); add_filter( 'ep_sync_insert_permissions_bypass', array( $this, 'filter_bypass_permission_checks_for_machines' ) ); @@ -315,6 +318,289 @@ public function action_sync_on_update( $post_id ) { } } + /** + * When a post's terms are changed, re-index. + * + * This catches term deletions via wp_delete_term(), because that function internally loops over all attached objects + * and updates their terms. It will also end up firing whenever set_object_terms is called, but the queue will de-duplicate + * multiple instances per post. This won't happen for taxonomies that has a default term (like Uncategorized for categories), + * hence why we also have `action_deleted_term_relationships`. + * + * @see set_object_terms + * @param int $post_id Post ID. + * @param array $terms An array of object terms. + * @param array $tt_ids An array of term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @param bool $append Whether to append new terms to the old terms. + * @param array $old_tt_ids Old array of term taxonomy IDs. + * @since 4.0.0 + */ + public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + if ( $this->kill_sync() ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + // Bypass saving if doing autosave + return; + } + + /** + * Filter to kill post sync + * + * @hook ep_post_sync_kill + * @param {bool} $skip True means kill sync for post + * @param {int} $post_id ID of post + * @param {int} $post_id ID of post + * @return {boolean} New value + */ + if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { + return; + } + + /** + * Filter to allow skipping this action in case of custom handling + * + * @hook ep_skip_action_set_object_terms + * @param {bool} $skip True means kill sync for post + * @param {int} $post_id ID of post + * @param {array} $terms An array of object terms. + * @param {array} $tt_ids An array of term taxonomy IDs. + * @param {string} $taxonomy Taxonomy slug. + * @param {bool} $append Whether to append new terms to the old terms. + * @param {array} $old_tt_ids Old array of term taxonomy IDs. + * @return {boolean} New value + */ + if ( apply_filters( 'ep_skip_action_set_object_terms', false, $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) ) { + return; + } + + $post = get_post( $post_id ); + if ( ! is_object( $post ) ) { + return; + } + + $indexable = Indexables::factory()->get( $this->indexable_slug ); + + // Check post status + $indexable_post_statuses = $indexable->get_indexable_post_status(); + if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { + return; + } + + // Only re-index if the taxonomy is indexed for this post + $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); + $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); + if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { + return; + } + + // Check post type + $indexable_post_types = $indexable->get_indexable_post_types(); + if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + return; + } + + /** + * Fire before post is queued for syncing + * + * @since 4.0.0 + * @hook ep_sync_on_set_object_terms + * @param {int} $post_id ID of post + * @param {array} $terms An array of object terms. + * @param {array} $tt_ids An array of term taxonomy IDs. + * @param {string} $taxonomy Taxonomy slug. + * @param {bool} $append Whether to append new terms to the old terms. + * @param {array} $old_tt_ids Old array of term taxonomy IDs. + */ + do_action( 'ep_sync_on_set_object_terms', $post_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ); + + $this->add_to_queue( $post_id ); + } + + /** + * When a term is updated, re-index all posts attached to that term + * + * @param int $term_id Term id. + * @param int $tt_id Term Taxonomy id. + * @param string $taxonomy Taxonomy name. + * @since 4.0.0 + */ + public function action_edited_term( $term_id, $tt_id, $taxonomy ) { + global $wpdb; + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + // Bypass saving if doing autosave + return; + } + + // Find ID of all attached posts (query lifted from wp_delete_term()) + $object_ids = (array) $wpdb->get_col( $wpdb->prepare( "SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d", $tt_id ) ); + + if ( ! count( $object_ids ) ) { + return; + } + + /** + * Filter to allow skipping this action in case of custom handling + * + * @hook ep_skip_action_edited_term + * @param {bool} $skip Current value of whether to skip running action_edited_term or not + * @param {int} $term_id Term id. + * @param {int} $tt_id Term Taxonomy id. + * @param {string} $taxonomy Taxonomy name. + * @param {array} $object_ids IDs of the objects attached to the term id. + * @return {bool} New value of whether to skip running action_edited_term or not + */ + if ( apply_filters( 'ep_skip_action_edited_term', false, $term_id, $tt_id, $taxonomy, $object_ids ) ) { + return; + } + + $indexable = Indexables::factory()->get( $this->indexable_slug ); + + // Add all of them to the queue + foreach ( $object_ids as $post_id ) { + /** + * Filter to kill post sync + * + * @hook ep_post_sync_kill + * @param {bool} $skip True meanas kill sync for post + * @param {int} $object_id ID of post + * @param {int} $object_id ID of post + * @return {boolean} New value + */ + if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { + return; + } + + $post = get_post( $post_id ); + + // If post not found, skip to the next iteration + if ( ! is_object( $post ) ) { + continue; + } + + // Check post status + $indexable_post_statuses = $indexable->get_indexable_post_status(); + if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { + continue; + } + + // Only re-index if the taxonomy is indexed for this post + $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); + $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); + if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { + continue; + } + + // Check post type + $indexable_post_types = $indexable->get_indexable_post_types(); + if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + continue; + } + + /** + * Fire before post is queued for syncing + * + * @hook ep_sync_on_edited_term + * @param {int} $post_id ID of post + * @param {int} $term_id ID of the term that was edited + * @param {int} $tt_id Taxonomy Term ID of the term that was edited + * @param {int} $taxonomy Taxonomy of the term that was edited + */ + do_action( 'ep_sync_on_edited_term', $post_id, $term_id, $tt_id, $taxonomy ); + + $this->add_to_queue( $post_id ); + } + } + + /** + * When a term relationship is deleted, re-index all posts attached to that term + * + * @param int $post_id Post ID. + * @param array $tt_ids An array of term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @since 4.0.0 + */ + public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy ) { + if ( $this->kill_sync() ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + // Bypass saving if doing autosave + return; + } + + /** + * Filter to kill post sync + * + * @hook ep_post_sync_kill + * @param {bool} $skip True meanas kill sync for post + * @param {int} $object_id ID of post + * @param {int} $object_id ID of post + * @return {boolean} New value + */ + if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { + return; + } + + /** + * Filter to allow skipping this action in case of custom handling + * + * @hook ep_skip_action_deleted_term_relationships + * @param {bool} $skip Current value of whether to skip running action_edited_term or not + * @param {int} $post_id Post ID. + * @param {array} $tt_ids An array of term taxonomy IDs. + * @param {string} $taxonomy Taxonomy slug. + * @return {bool} New value of whether to skip running action_deleted_term_relationships or not + */ + if ( apply_filters( 'ep_skip_action_deleted_term_relationships', false, $post_id, $tt_ids, $taxonomy ) ) { + return; + } + + $post = get_post( $post_id ); + + // If post not found, skip to the next iteration + if ( ! is_object( $post ) ) { + return; + } + + $indexable = Indexables::factory()->get( $this->indexable_slug ); + + // Check post status + $indexable_post_statuses = $indexable->get_indexable_post_status(); + if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { + return; + } + + // Only re-index if the taxonomy is indexed for this post + $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); + $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); + if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { + return; + } + + // Check post type + $indexable_post_types = $indexable->get_indexable_post_types(); + if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + return; + } + + /** + * Fire before post is queued for syncing + * + * @hook ep_sync_on_deleted_term_relationships + * @since 4.0.0 + * @param {int} $post_id ID of post + * @param {array} $tt_ids An array of term taxonomy IDs. + * @param {string} $taxonomy Taxonomy of the term that was edited + */ + do_action( 'ep_sync_on_deleted_term_relationships', $post_id, $tt_ids, $taxonomy ); + + $this->add_to_queue( $post_id ); + } + /** * Create mapping and network alias when a new blog is created. * diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php index 4a77e0d468..df2ffee41d 100644 --- a/tests/php/indexables/TestPost.php +++ b/tests/php/indexables/TestPost.php @@ -6657,4 +6657,74 @@ public function testInsertPostAndDeleteAnother() { $this->assertEquals( 1, $query->found_posts ); $this->assertEquals( $query->posts[0]->ID, $new_post_id ); } + + /** + * Tests term deletion applied to posts + * + * @return void + * @group post + */ + public function testPostDeletedTerm() { + $cat = wp_create_category( 'test category' ); + $tag = wp_insert_category( [ 'taxonomy' => 'post_tag', 'cat_name' => 'test-tag' ] ); + + $post_id = Functions\create_and_sync_post( + array( + 'tags_input' => array( $tag ), + 'post_category' => array( $cat ), + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id ); + $this->assertNotEmpty( $document['terms']['category'] ); + $this->assertNotEmpty( $document['terms']['post_tag'] ); + + ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->sync_queue = []; + + wp_delete_term( $tag, 'post_tag' ); + wp_delete_term( $cat, 'category' ); + + ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->index_sync_queue(); + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id ); + // Category will fallback to Uncategorized. + $this->assertNotContains( $cat, wp_list_pluck( $document['terms']['category'], 'term_id' ) ); + $this->assertArrayNotHasKey( 'post_tag', $document['terms'] ); + } + + /** + * Tests term edition applied to posts + * + * @return void + * @group post + */ + public function testPostEditedTerm() { + $post_id = Functions\create_and_sync_post( + array( + 'tags_input' => array( 'test-tag' ), + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $test_tag = get_term_by( 'name', 'test-tag', 'post_tag' ); + wp_update_term( + $test_tag->term_id, + 'post_tag', + [ + 'slug' => 'different-tag-slug', + 'name' => 'Different Tag Name', + ] + ); + + ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->index_sync_queue(); + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $document = ElasticPress\Indexables::factory()->get( 'post' )->get( $post_id ); + $this->assertEquals( 'different-tag-slug', $document['terms']['post_tag'][0]['slug'] ); + $this->assertEquals( 'Different Tag Name', $document['terms']['post_tag'][0]['name'] ); + } } From 86ab2e9ae0144bbfa933c3b79b5e21900ef837cd Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 21 Feb 2022 14:13:11 -0300 Subject: [PATCH 2/2] New method to check if a post should be queued or not --- .../classes/Indexable/Post/SyncManager.php | 163 ++++++------------ 1 file changed, 53 insertions(+), 110 deletions(-) diff --git a/includes/classes/Indexable/Post/SyncManager.php b/includes/classes/Indexable/Post/SyncManager.php index 7777d35c2e..72cb7a5253 100644 --- a/includes/classes/Indexable/Post/SyncManager.php +++ b/includes/classes/Indexable/Post/SyncManager.php @@ -345,19 +345,6 @@ public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $ return; } - /** - * Filter to kill post sync - * - * @hook ep_post_sync_kill - * @param {bool} $skip True means kill sync for post - * @param {int} $post_id ID of post - * @param {int} $post_id ID of post - * @return {boolean} New value - */ - if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { - return; - } - /** * Filter to allow skipping this action in case of custom handling * @@ -375,29 +362,7 @@ public function action_set_object_terms( $post_id, $terms, $tt_ids, $taxonomy, $ return; } - $post = get_post( $post_id ); - if ( ! is_object( $post ) ) { - return; - } - - $indexable = Indexables::factory()->get( $this->indexable_slug ); - - // Check post status - $indexable_post_statuses = $indexable->get_indexable_post_status(); - if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { - return; - } - - // Only re-index if the taxonomy is indexed for this post - $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); - $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); - if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { - return; - } - - // Check post type - $indexable_post_types = $indexable->get_indexable_post_types(); - if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) { return; } @@ -460,42 +425,7 @@ public function action_edited_term( $term_id, $tt_id, $taxonomy ) { // Add all of them to the queue foreach ( $object_ids as $post_id ) { - /** - * Filter to kill post sync - * - * @hook ep_post_sync_kill - * @param {bool} $skip True meanas kill sync for post - * @param {int} $object_id ID of post - * @param {int} $object_id ID of post - * @return {boolean} New value - */ - if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { - return; - } - - $post = get_post( $post_id ); - - // If post not found, skip to the next iteration - if ( ! is_object( $post ) ) { - continue; - } - - // Check post status - $indexable_post_statuses = $indexable->get_indexable_post_status(); - if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { - continue; - } - - // Only re-index if the taxonomy is indexed for this post - $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); - $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); - if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { - continue; - } - - // Check post type - $indexable_post_types = $indexable->get_indexable_post_types(); - if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) { continue; } @@ -532,19 +462,6 @@ public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy return; } - /** - * Filter to kill post sync - * - * @hook ep_post_sync_kill - * @param {bool} $skip True meanas kill sync for post - * @param {int} $object_id ID of post - * @param {int} $object_id ID of post - * @return {boolean} New value - */ - if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { - return; - } - /** * Filter to allow skipping this action in case of custom handling * @@ -559,31 +476,7 @@ public function action_deleted_term_relationships( $post_id, $tt_ids, $taxonomy return; } - $post = get_post( $post_id ); - - // If post not found, skip to the next iteration - if ( ! is_object( $post ) ) { - return; - } - - $indexable = Indexables::factory()->get( $this->indexable_slug ); - - // Check post status - $indexable_post_statuses = $indexable->get_indexable_post_status(); - if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { - return; - } - - // Only re-index if the taxonomy is indexed for this post - $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); - $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); - if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { - return; - } - - // Check post type - $indexable_post_types = $indexable->get_indexable_post_types(); - if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + if ( ! $this->should_reindex_post( $post_id, $taxonomy ) ) { return; } @@ -631,4 +524,54 @@ public function action_create_blog_index( $blog ) { restore_current_blog(); } + + /** + * Check if post attributes (post status, taxonomy, and type) match what is needed to reindex or not. + * + * @param int $post_id The post ID. + * @param string $taxonomy The taxonomy slug. + * @return boolean + */ + protected function should_reindex_post( $post_id, $taxonomy ) { + /** + * Filter to kill post sync + * + * @hook ep_post_sync_kill + * @param {bool} $skip True meanas kill sync for post + * @param {int} $object_id ID of post + * @param {int} $object_id ID of post + * @return {boolean} New value + */ + if ( apply_filters( 'ep_post_sync_kill', false, $post_id, $post_id ) ) { + return false; + } + + $post = get_post( $post_id ); + if ( ! is_object( $post ) ) { + return false; + } + + $indexable = Indexables::factory()->get( $this->indexable_slug ); + + // Check post status + $indexable_post_statuses = $indexable->get_indexable_post_status(); + if ( ! in_array( $post->post_status, $indexable_post_statuses, true ) ) { + return false; + } + + // Only re-index if the taxonomy is indexed for this post + $indexable_taxonomies = $indexable->get_indexable_post_taxonomies( $post ); + $indexable_taxonomy_names = wp_list_pluck( $indexable_taxonomies, 'name' ); + if ( ! in_array( $taxonomy, $indexable_taxonomy_names, true ) ) { + return false; + } + + // Check post type + $indexable_post_types = $indexable->get_indexable_post_types(); + if ( ! in_array( $post->post_type, $indexable_post_types, true ) ) { + return false; + } + + return true; + } }