diff --git a/projects/packages/sync/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/packages/sync/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/packages/sync/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/packages/sync/src/class-defaults.php b/projects/packages/sync/src/class-defaults.php index cfcd7f695f39b..5a24ba6f6915a 100644 --- a/projects/packages/sync/src/class-defaults.php +++ b/projects/packages/sync/src/class-defaults.php @@ -811,6 +811,33 @@ public static function get_post_meta_whitelist() { return apply_filters( 'jetpack_sync_post_meta_whitelist', self::$post_meta_whitelist ); } + /** + * Array of post meta key prefixes whitelisted (wildcard match). + * + * @var array Post meta prefix whitelist. + */ + public static $post_meta_prefix_whitelist = array( + '_wpas_skip_', + ); + + /** + * Get the post meta key prefix whitelist (wildcard match). + * + * @return array Post meta prefix whitelist. + */ + public static function get_post_meta_prefix_whitelist() { + /** + * Filter the list of post meta key prefixes (wildcards) that are manageable via the JSON API. + * + * @module sync + * + * @since $$next_version + * + * @param array The default list of meta key prefixes. + */ + return apply_filters( 'jetpack_sync_post_meta_prefix_whitelist', self::$post_meta_prefix_whitelist ); + } + /** * Comment meta whitelist. * diff --git a/projects/packages/sync/src/class-settings.php b/projects/packages/sync/src/class-settings.php index f248cb8b0fa79..a391b8c5c2bbd 100644 --- a/projects/packages/sync/src/class-settings.php +++ b/projects/packages/sync/src/class-settings.php @@ -425,8 +425,17 @@ public static function get_whitelisted_post_meta_sql() { public static function get_allowed_post_meta_structured() { return array( 'meta_key' => array( - 'operator' => 'IN', - 'values' => array_map( 'esc_sql', static::get_setting( 'post_meta_whitelist' ) ), + 'group_operator' => 'OR', + 'filters' => array( + array( + 'operator' => 'IN', + 'values' => array_map( 'esc_sql', static::get_setting( 'post_meta_whitelist' ) ), + ), + array( + 'operator' => 'LIKE', + 'values' => array_map( 'esc_sql', Defaults::get_post_meta_prefix_whitelist() ), + ), + ), ), ); } diff --git a/projects/packages/sync/src/modules/class-module.php b/projects/packages/sync/src/modules/class-module.php index 0e03841fbde13..eec9e090b05b2 100644 --- a/projects/packages/sync/src/modules/class-module.php +++ b/projects/packages/sync/src/modules/class-module.php @@ -584,12 +584,13 @@ protected function get_chunks_with_preceding_end( $chunks, $previous_interval_en * * @todo Refactor to use $wpdb->prepare() on the SQL query. * - * @param array $ids Object IDs. - * @param string $meta_type Meta type. - * @param array $meta_key_whitelist Meta key whitelist. + * @param array $ids Object IDs. + * @param string $meta_type Meta type. + * @param array $meta_key_whitelist Exact meta keys to allow. + * @param array $meta_key_prefix_filters Optional. Prefixes of meta keys to allow. * @return array Unserialized meta values. */ - protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { + protected function get_metadata( $ids, $meta_type, $meta_key_whitelist, $meta_key_prefix_filters = array() ) { global $wpdb; $table = _get_meta_table( $meta_type ); $id = $meta_type . '_id'; @@ -597,18 +598,33 @@ protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { return array(); } - $private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'"; - - return array_map( - array( $this, 'unserialize_meta' ), - $wpdb->get_results( - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared - "SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )' . - " AND meta_key IN ( $private_meta_whitelist_sql ) ", - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared - OBJECT - ) + $meta_key_conditions = array(); + + if ( ! empty( $meta_key_whitelist ) ) { + $escaped_keys = implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ); + $meta_key_conditions[] = "meta_key IN ( '$escaped_keys' )"; + } + + foreach ( $meta_key_prefix_filters as $prefix ) { + $meta_key_conditions[] = $wpdb->prepare( 'meta_key LIKE %s', esc_sql( $prefix ) . '%' ); + } + + if ( empty( $meta_key_conditions ) ) { + return array(); // No whitelist or wildcard = deny all + } + + $ids_sql = implode( ',', wp_parse_id_list( $ids ) ); + $where_sql = implode( ' OR ', $meta_key_conditions ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( + "SELECT $id, meta_key, meta_value, meta_id + FROM $table + WHERE $id IN ( $ids_sql ) + AND ( $where_sql )", + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + OBJECT ); + return array_map( array( $this, 'unserialize_meta' ), $results ); } /** diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index e74b0a24293f9..a0ab25d8102a5 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -9,6 +9,7 @@ use Automattic\Jetpack\Constants as Jetpack_Constants; use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Sync\Defaults; use Automattic\Jetpack\Sync\Modules; use Automattic\Jetpack\Sync\Settings; @@ -397,11 +398,23 @@ public function filter_meta( $args ) { * Whether a post meta key is whitelisted. * * @param string $meta_key Meta key. - * @return boolean Whether the post meta key is whitelisted. + * @return bool Whether the post meta key is whitelisted. */ public function is_whitelisted_post_meta( $meta_key ) { - // The _wpas_skip_ meta key is used by Publicize. - return in_array( $meta_key, Settings::get_setting( 'post_meta_whitelist' ), true ) || str_starts_with( $meta_key, '_wpas_skip_' ); + $whitelist = Settings::get_setting( 'post_meta_whitelist' ); + $prefix_whitelist = Defaults::get_post_meta_prefix_whitelist(); + + if ( in_array( $meta_key, $whitelist, true ) ) { + return true; + } + + foreach ( $prefix_whitelist as $prefix ) { + if ( str_starts_with( $meta_key, $prefix ) ) { + return true; + } + } + + return false; } /** @@ -843,7 +856,7 @@ public function expand_posts_with_metadata_and_terms( $args ) { list( $post_ids, $previous_interval_end ) = $args; $posts = $this->expand_posts( $post_ids ); - $posts_metadata = $this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ); + $posts_metadata = $this->get_metadata( $post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ), Defaults::get_post_meta_prefix_whitelist() ); $term_relationships = $this->get_term_relationships( $post_ids ); return array( @@ -898,7 +911,7 @@ public function get_next_chunk( $config, $status, $chunk_size ) { } // Get the post IDs from the posts that were fetched. $fetched_post_ids = wp_list_pluck( $posts, 'ID' ); - $metadata = $this->get_metadata( $fetched_post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ) ); + $metadata = $this->get_metadata( $fetched_post_ids, 'post', Settings::get_setting( 'post_meta_whitelist' ), Defaults::get_post_meta_prefix_whitelist() ); // Filter the posts and metadata based on the maximum size constraints. list( $filtered_post_ids, $filtered_posts, $filtered_posts_metadata ) = $this->filter_objects_and_metadata_by_size( diff --git a/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php b/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php index 496fbff360755..7e9157bc1a86b 100644 --- a/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php +++ b/projects/packages/sync/src/modules/class-woocommerce-hpos-orders.php @@ -471,10 +471,11 @@ private static function wc_get_order_status_keys() { * @param array $ids List of order IDs. * @param string $meta_type Meta type. * @param array $meta_key_whitelist List of allowed meta keys. + * @param array $meta_key_prefix_filters Optional. Prefixes of meta keys to allow. * * @return array Filtered order metadata. */ - protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- returning empty meta is intentional. + protected function get_metadata( $ids, $meta_type, $meta_key_whitelist, $meta_key_prefix_filters = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- returning empty meta is intentional. return array(); // don't sync metadata, all allow-listed core data is available in the order object. } diff --git a/projects/packages/sync/src/replicastore/class-table-checksum.php b/projects/packages/sync/src/replicastore/class-table-checksum.php index dc6b92177cef5..fcfc02b7080e9 100644 --- a/projects/packages/sync/src/replicastore/class-table-checksum.php +++ b/projects/packages/sync/src/replicastore/class-table-checksum.php @@ -521,21 +521,42 @@ protected function prepare_filter_values_as_sql( $filter_values = array(), $tabl $result = array(); - foreach ( $filter_values as $field => $filter ) { + foreach ( $filter_values as $field => $filters ) { $key = ( ! empty( $table_prefix ) ? $table_prefix : $this->table ) . '.' . $field; - switch ( $filter['operator'] ) { - case 'IN': - case 'NOT IN': - $filter_values_count = is_countable( $filter['values'] ) ? count( $filter['values'] ) : 0; - $values_placeholders = implode( ',', array_fill( 0, $filter_values_count, '%s' ) ); - $statement = "{$key} {$filter['operator']} ( $values_placeholders )"; + $group_operator = $filters['group_operator'] ?? 'AND'; // Default to AND if not specified. + // If filters is explicitly provided, separate it out + if ( isset( $filters['filters'] ) ) { + $filters = $filters['filters']; + } else { + $filters = array( $filters ); + } - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $prepared_statement = $wpdb->prepare( $statement, $filter['values'] ); + $group_clauses = array(); + + foreach ( $filters as $filter ) { + switch ( $filter['operator'] ) { + case 'IN': + case 'NOT IN': + $filter_values_count = is_countable( $filter['values'] ) ? count( $filter['values'] ) : 0; + $values_placeholders = implode( ',', array_fill( 0, $filter_values_count, '%s' ) ); + $statement = "{$key} {$filter['operator']} ( $values_placeholders )"; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $group_clauses[] = $wpdb->prepare( $statement, $filter['values'] ); + break; + + case 'LIKE': + foreach ( $filter['values'] as $wildcard ) { + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $group_clauses[] = $wpdb->prepare( "{$key} LIKE %s", $wildcard . '%' ); + } + break; + } + } - $result[] = $prepared_statement; - break; + if ( ! empty( $group_clauses ) ) { + $result[] = '( ' . implode( " {$group_operator} ", $group_clauses ) . ' )'; } } diff --git a/projects/plugins/automattic-for-agencies-client/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/automattic-for-agencies-client/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/automattic-for-agencies-client/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/backup/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/backup/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/backup/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/boost/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/boost/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/boost/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/jetpack/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/jetpack/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..82c56e40e13d8 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/mu-wpcom-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/mu-wpcom-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/mu-wpcom-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/protect/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/protect/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/protect/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/search/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/search/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/search/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/social/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/social/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/social/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/starter-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/starter-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/starter-plugin/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/videopress/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/videopress/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/videopress/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums. diff --git a/projects/plugins/wpcomsh/changelog/update-sync-allow-post-meta-prefix-whitelist b/projects/plugins/wpcomsh/changelog/update-sync-allow-post-meta-prefix-whitelist new file mode 100644 index 0000000000000..4f4b19de3bb8c --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/update-sync-allow-post-meta-prefix-whitelist @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Sync: Allowing post meta prefix whitelist that works for Full Sync and Checksums.