From 02e231f27e5802793a1b4475f9b9e9e5292726ce Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Thu, 13 Nov 2025 13:28:36 +0000 Subject: [PATCH 1/7] Only send a single updated_post_meta action per attachment metadata request --- .../packages/sync/src/modules/class-posts.php | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index e74b0a24293f9..330e46610ef85 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -227,7 +227,7 @@ public function init_full_sync_listeners( $callable ) { public function init_before_send() { // meta. add_filter( 'jetpack_sync_before_send_added_post_meta', array( $this, 'trim_post_meta' ) ); - add_filter( 'jetpack_sync_before_send_updated_post_meta', array( $this, 'trim_post_meta' ) ); + add_filter( 'jetpack_sync_before_send_updated_post_meta', array( $this, 'filter_updated_post_meta_before_send' ), 5 ); // Incase this filter is used elsewhere, we run early. add_filter( 'jetpack_sync_before_send_deleted_post_meta', array( $this, 'trim_post_meta' ) ); // Full sync. $sync_module = Modules::get_module( 'full-sync' ); @@ -322,6 +322,78 @@ public function trim_post_meta( $args ) { return array( $meta_id, $object_id, $meta_key, $meta_value ); } + /** + * Drop stale _wp_attachment_metadata updates at send-time by comparing against current database value. + * + * @param array $args The hook arguments: [ $meta_id, $object_id, $meta_key, $meta_value ]. + * @return array|false Return original args to send, or false to skip if stale. + */ + public function drop_stale_attachment_metadata( $args ) { + if ( ! is_array( $args ) || count( $args ) < 4 ) { // The updated_postmeta do_aciton from core always sends four args. + return $args; + } + list( , $post_id, , $meta_value ) = $args; + + static $post_meta_cached = array(); + $post_id = (int) $post_id; + + if ( isset( $post_meta_cached[ $post_id ] ) ) { + $cached_value = $post_meta_cached[ $post_id ]['value']; + $cached_signature = $post_meta_cached[ $post_id ]['signature']; + } else { + $current_value = wp_get_attachment_metadata( $post_id, true ); + if ( ! is_array( $current_value ) ) { + return $args; + } + $cached_signature = wp_json_encode( $current_value ); + $post_meta_cached[ $post_id ] = array( + 'value' => $current_value, + 'signature' => $cached_signature, + ); + } + + if ( is_string( $meta_value ) ) { + $meta_value = maybe_unserialize( $meta_value ); + } + if ( ! is_array( $meta_value ) ) { + return $args; + } + + $queued_signature = wp_json_encode( $meta_value ); + + if ( $queued_signature === false ) { + return $args; + } + + if ( $queued_signature !== $cached_signature ) { + return false; + } + + // Use current DB payload to ensure freshest data gets sent. + $args[3] = $cached_value; + return $args; + } + + /** + * Unified handler for updated post meta before send: skips stale _wp_attachment_metadata and trims meta. + * + * @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ]. + * @return array|false + */ + public function filter_updated_post_meta_before_send( $args ) { + if ( is_array( $args ) && count( $args ) >= 3 ) { + $meta_key = $args[2]; + if ( '_wp_attachment_metadata' !== $meta_key ) { + return $this->trim_post_meta( $args ); + } + } + $args = $this->drop_stale_attachment_metadata( $args ); + if ( false === $args ) { + return false; + } + return $this->trim_post_meta( $args ); + } + /** * Process content before send. * @@ -382,10 +454,13 @@ public function filter_blacklisted_post_types_deleted( $args ) { /** * Filter all meta that is not blacklisted, or is stored for a disallowed post type. * - * @param array $args Hook arguments. + * @param array|false $args Hook arguments. * @return array|false Hook arguments, or false if meta was filtered. */ public function filter_meta( $args ) { + if ( ! is_array( $args ) ) { + return false; + } if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) { return $args; } From ddd8d67209b1a459ba01846cde25f67a2a61120a Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Thu, 13 Nov 2025 13:33:25 +0000 Subject: [PATCH 2/7] changelog --- .../changelog/update-sync-class-posts-attachment-metadata | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/sync/changelog/update-sync-class-posts-attachment-metadata diff --git a/projects/packages/sync/changelog/update-sync-class-posts-attachment-metadata b/projects/packages/sync/changelog/update-sync-class-posts-attachment-metadata new file mode 100644 index 0000000000000..6a4c0c5a3826a --- /dev/null +++ b/projects/packages/sync/changelog/update-sync-class-posts-attachment-metadata @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Jetpack Sync: Only send a single updated_post_meta action per attachment metadata request. From c40119035fef10f4d34d9e8e224daa046e8662a2 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Thu, 13 Nov 2025 14:47:25 +0000 Subject: [PATCH 3/7] Ensure we fetch filtered metadata, as well as add a per request guard --- .../packages/sync/src/modules/class-posts.php | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index 330e46610ef85..fffd5270315b8 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -323,54 +323,30 @@ public function trim_post_meta( $args ) { } /** - * Drop stale _wp_attachment_metadata updates at send-time by comparing against current database value. + * Drop bursts for _wp_attachment_metadata at send-time by sending the current DB state once per request. * * @param array $args The hook arguments: [ $meta_id, $object_id, $meta_key, $meta_value ]. - * @return array|false Return original args to send, or false to skip if stale. + * @return array|false Return args to send once per attachment per request, or false to skip subsequent ones. */ public function drop_stale_attachment_metadata( $args ) { - if ( ! is_array( $args ) || count( $args ) < 4 ) { // The updated_postmeta do_aciton from core always sends four args. + if ( ! is_array( $args ) || count( $args ) < 4 ) { // The updated_postmeta do_action from core always sends four args. return $args; } - list( , $post_id, , $meta_value ) = $args; + list( , $post_id, ) = $args; - static $post_meta_cached = array(); + static $sent_for_request = array(); $post_id = (int) $post_id; - - if ( isset( $post_meta_cached[ $post_id ] ) ) { - $cached_value = $post_meta_cached[ $post_id ]['value']; - $cached_signature = $post_meta_cached[ $post_id ]['signature']; - } else { - $current_value = wp_get_attachment_metadata( $post_id, true ); - if ( ! is_array( $current_value ) ) { - return $args; - } - $cached_signature = wp_json_encode( $current_value ); - $post_meta_cached[ $post_id ] = array( - 'value' => $current_value, - 'signature' => $cached_signature, - ); - } - - if ( is_string( $meta_value ) ) { - $meta_value = maybe_unserialize( $meta_value ); - } - if ( ! is_array( $meta_value ) ) { - return $args; - } - - $queued_signature = wp_json_encode( $meta_value ); - - if ( $queued_signature === false ) { - return $args; + if ( isset( $sent_for_request[ $post_id ] ) ) { + return false; } - if ( $queued_signature !== $cached_signature ) { - return false; + // Fetch current metadata (filtered) to honor site/plugin filters on wp_get_attachment_metadata. + $current_value = wp_get_attachment_metadata( $post_id ); + if ( is_array( $current_value ) && ! empty( $current_value ) ) { + $args[3] = $current_value; // Ensure freshest state is sent. } - // Use current DB payload to ensure freshest data gets sent. - $args[3] = $cached_value; + $sent_for_request[ $post_id ] = true; return $args; } From c8a9f99d57dd9a78d4efacf551af1ee6b2f7c002 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Thu, 13 Nov 2025 17:06:45 +0000 Subject: [PATCH 4/7] Cleanup a function comment --- projects/packages/sync/src/modules/class-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index fffd5270315b8..fe71834d60cc8 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -323,7 +323,7 @@ public function trim_post_meta( $args ) { } /** - * Drop bursts for _wp_attachment_metadata at send-time by sending the current DB state once per request. + * Prevent multiples of _wp_attachment_metadata at send-time by sending the current DB state once per request. * * @param array $args The hook arguments: [ $meta_id, $object_id, $meta_key, $meta_value ]. * @return array|false Return args to send once per attachment per request, or false to skip subsequent ones. From 156cb1d254254884e547754870162560d5bb89d8 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Thu, 13 Nov 2025 18:59:39 +0000 Subject: [PATCH 5/7] Add additional defensive count check in filter_meta --- projects/packages/sync/src/modules/class-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index fe71834d60cc8..882ef6a9fab4d 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -434,7 +434,7 @@ public function filter_blacklisted_post_types_deleted( $args ) { * @return array|false Hook arguments, or false if meta was filtered. */ public function filter_meta( $args ) { - if ( ! is_array( $args ) ) { + if ( ! is_array( $args ) || count( $args ) < 3 ) { return false; } if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) { From 2030bfca5b8eb39ca428cf4ef265ad2058da2b09 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Fri, 14 Nov 2025 14:33:27 +0000 Subject: [PATCH 6/7] Ensure we dedupe per post id at enqueue time --- .../packages/sync/src/modules/class-posts.php | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index 882ef6a9fab4d..1a4fc75654afa 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -158,6 +158,8 @@ public function init_listeners( $callable ) { $this->init_listeners_for_meta_type( 'post', $callable ); $this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) ); + add_filter( 'jetpack_sync_before_enqueue_updated_post_meta', array( $this, 'on_before_enqueue_updated_attachment_metadata' ), 1 ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_jetpack_sync_before_enqueue_jetpack_sync_save_post' ) ); add_filter( 'jetpack_sync_before_enqueue_jetpack_published_post', array( $this, 'filter_jetpack_sync_before_enqueue_jetpack_published_post' ) ); @@ -226,7 +228,7 @@ public function init_full_sync_listeners( $callable ) { */ public function init_before_send() { // meta. - add_filter( 'jetpack_sync_before_send_added_post_meta', array( $this, 'trim_post_meta' ) ); + add_filter( 'jetpack_sync_before_send_added_post_meta', array( $this, 'filter_added_post_meta_before_send' ), 5 ); // Incase this filter is used elsewhere, we run early. add_filter( 'jetpack_sync_before_send_updated_post_meta', array( $this, 'filter_updated_post_meta_before_send' ), 5 ); // Incase this filter is used elsewhere, we run early. add_filter( 'jetpack_sync_before_send_deleted_post_meta', array( $this, 'trim_post_meta' ) ); // Full sync. @@ -323,51 +325,73 @@ public function trim_post_meta( $args ) { } /** - * Prevent multiples of _wp_attachment_metadata at send-time by sending the current DB state once per request. + * Updated post meta send-time filter: refreshes _wp_attachment_metadata to the latest DB value, then trims. * - * @param array $args The hook arguments: [ $meta_id, $object_id, $meta_key, $meta_value ]. - * @return array|false Return args to send once per attachment per request, or false to skip subsequent ones. + * @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ]. + * @return array Filtered args. */ - public function drop_stale_attachment_metadata( $args ) { - if ( ! is_array( $args ) || count( $args ) < 4 ) { // The updated_postmeta do_action from core always sends four args. + public function filter_updated_post_meta_before_send( $args ) { + if ( ! is_array( $args ) || count( $args ) < 4 ) { return $args; } - list( , $post_id, ) = $args; - - static $sent_for_request = array(); - $post_id = (int) $post_id; - if ( isset( $sent_for_request[ $post_id ] ) ) { - return false; + list( $meta_id, $object_id, $meta_key, $meta_value ) = $args; + if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( (int) $object_id ) ) { + return $this->trim_post_meta( $args ); } - - // Fetch current metadata (filtered) to honor site/plugin filters on wp_get_attachment_metadata. - $current_value = wp_get_attachment_metadata( $post_id ); + $current_value = wp_get_attachment_metadata( (int) $object_id ); if ( is_array( $current_value ) && ! empty( $current_value ) ) { - $args[3] = $current_value; // Ensure freshest state is sent. + $meta_value = $current_value; } + return $this->trim_post_meta( array( $meta_id, $object_id, $meta_key, $meta_value ) ); + } - $sent_for_request[ $post_id ] = true; - return $args; + /** + * Added post meta send-time filter: refreshes _wp_attachment_metadata to the latest DB value, then trims. + * + * @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ]. + * @return array Filtered args. + */ + public function filter_added_post_meta_before_send( $args ) { + if ( ! is_array( $args ) || count( $args ) < 4 ) { + return $args; + } + list( $meta_id, $object_id, $meta_key, $meta_value ) = $args; + if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( (int) $object_id ) ) { + return $this->trim_post_meta( $args ); + } + $current_value = wp_get_attachment_metadata( (int) $object_id ); + // For added_post_meta, skip clearly incomplete snapshots (e.g., missing or empty sizes). + if ( ! is_array( $current_value ) || empty( $current_value ) ) { + return false; + } + if ( isset( $current_value['sizes'] ) && is_array( $current_value['sizes'] ) && count( $current_value['sizes'] ) === 0 ) { + return false; + } + $meta_value = $current_value; + return $this->trim_post_meta( array( $meta_id, $object_id, $meta_key, $meta_value ) ); } /** - * Unified handler for updated post meta before send: skips stale _wp_attachment_metadata and trims meta. + * Enqueue-time per-request dedupe for updated attachment metadata. * * @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ]. * @return array|false */ - public function filter_updated_post_meta_before_send( $args ) { - if ( is_array( $args ) && count( $args ) >= 3 ) { - $meta_key = $args[2]; - if ( '_wp_attachment_metadata' !== $meta_key ) { - return $this->trim_post_meta( $args ); - } + public function on_before_enqueue_updated_attachment_metadata( $args ) { + if ( ! is_array( $args ) || count( $args ) < 3 ) { + return $args; } - $args = $this->drop_stale_attachment_metadata( $args ); - if ( false === $args ) { + $post_id = (int) $args[1]; + $meta_key = $args[2]; + if ( '_wp_attachment_metadata' !== $meta_key || 'attachment' !== get_post_type( $post_id ) ) { + return $args; + } + static $seen_updated_meta_for_post = array(); + if ( isset( $seen_updated_meta_for_post[ $post_id ] ) ) { return false; } - return $this->trim_post_meta( $args ); + $seen_updated_meta_for_post[ $post_id ] = true; + return $args; } /** From 8bd8444b534f501632f4927dc582e48af280114a Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Fri, 14 Nov 2025 14:50:43 +0000 Subject: [PATCH 7/7] Fix docblock return value to allow for a false value --- projects/packages/sync/src/modules/class-posts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index 1a4fc75654afa..b261a8f17ff6a 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -349,7 +349,7 @@ public function filter_updated_post_meta_before_send( $args ) { * Added post meta send-time filter: refreshes _wp_attachment_metadata to the latest DB value, then trims. * * @param array $args [ $meta_id, $object_id, $meta_key, $meta_value ]. - * @return array Filtered args. + * @return array|false Filtered args, or false to skip sending when the snapshot is clearly incomplete. */ public function filter_added_post_meta_before_send( $args ) { if ( ! is_array( $args ) || count( $args ) < 4 ) {