updated plugin Jetpack Protect version 5.0.0
This commit is contained in:
@ -175,6 +175,7 @@ class Actions {
|
||||
) ) {
|
||||
self::initialize_sender();
|
||||
add_action( 'shutdown', array( self::$sender, 'do_sync' ), 9998 );
|
||||
|
||||
if ( self::should_initialize_sender( true ) ) {
|
||||
add_action( 'shutdown', array( self::$sender, 'do_full_sync' ), 9999 );
|
||||
}
|
||||
@ -362,6 +363,10 @@ class Actions {
|
||||
|
||||
$queue = self::$sender->get_sync_queue();
|
||||
$full_queue = self::$sender->get_full_sync_queue();
|
||||
// We are sending the expiry vs the actual dedicated lock value to ensure backwards compatibility
|
||||
// with previous versions where the lock value was a timestamp.
|
||||
$dedicated_sync_lock_option_name = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
|
||||
$dedicated_sync_lock_expires_name = $dedicated_sync_lock_option_name . '_expires';
|
||||
|
||||
$debug['debug_details']['sync_locks'] = array(
|
||||
'retry_time_sync' => get_option( self::RETRY_AFTER_PREFIX . 'sync' ),
|
||||
@ -370,7 +375,7 @@ class Actions {
|
||||
'next_sync_time_full_sync' => self::$sender->get_next_sync_time( 'full_sync' ),
|
||||
'queue_locked_sync' => $queue->is_locked(),
|
||||
'queue_locked_full_sync' => $full_queue->is_locked(),
|
||||
'dedicated_sync_request_lock' => \Jetpack_Options::get_raw_option( Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null ),
|
||||
'dedicated_sync_request_lock' => \Jetpack_Options::get_raw_option( $dedicated_sync_lock_expires_name, null ),
|
||||
'dedicated_sync_temporary_disable_flag' => get_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG ),
|
||||
);
|
||||
|
||||
@ -666,11 +671,9 @@ class Actions {
|
||||
*/
|
||||
public static function jetpack_cron_schedule( $schedules ) {
|
||||
if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) {
|
||||
$minutes = (int) ( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 );
|
||||
$display = ( 1 === $minutes ) ?
|
||||
__( 'Every minute', 'jetpack-sync' ) :
|
||||
/* translators: %d is an integer indicating the number of minutes. */
|
||||
sprintf( __( 'Every %d minutes', 'jetpack-sync' ), $minutes );
|
||||
$minutes = ( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 );
|
||||
/* translators: %d is an integer indicating the number of minutes. */
|
||||
$display = sprintf( __( 'Every %d minutes', 'jetpack-sync' ), $minutes );
|
||||
$schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array(
|
||||
'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE,
|
||||
'display' => $display,
|
||||
@ -686,7 +689,45 @@ class Actions {
|
||||
* @static
|
||||
*/
|
||||
public static function do_cron_sync() {
|
||||
self::do_cron_sync_by_type( 'sync' );
|
||||
if ( ! self::sync_allowed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::initialize_sender();
|
||||
|
||||
$time_limit = Settings::get_setting( 'cron_sync_time_limit' );
|
||||
$start_time = time();
|
||||
$executions = 0;
|
||||
|
||||
$lock_id = Dedicated_Sender::try_lock_spawn_request();
|
||||
|
||||
do {
|
||||
$next_sync_time = self::$sender->get_next_sync_time( 'sync' );
|
||||
|
||||
if ( $next_sync_time ) {
|
||||
$delay = $next_sync_time - time() + 1;
|
||||
if ( $delay > 15 ) {
|
||||
break;
|
||||
} elseif ( $delay > 0 ) {
|
||||
sleep( (int) $delay );
|
||||
}
|
||||
}
|
||||
|
||||
$result = self::$sender->do_sync_and_set_delays( self::$sender->get_sync_queue() );
|
||||
|
||||
if ( is_wp_error( $result ) && in_array( $result->get_error_code(), array( 'unclosed_buffer', 'sync_throttled' ), true ) ) {
|
||||
$result = true; // Give it some time.
|
||||
}
|
||||
// # of send actions performed.
|
||||
++$executions;
|
||||
|
||||
} while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
|
||||
|
||||
if ( $lock_id ) {
|
||||
Dedicated_Sender::try_release_lock_spawn_request( $lock_id );
|
||||
}
|
||||
|
||||
return $executions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -696,7 +737,31 @@ class Actions {
|
||||
* @static
|
||||
*/
|
||||
public static function do_cron_full_sync() {
|
||||
self::do_cron_sync_by_type( 'full_sync' );
|
||||
if ( ! self::sync_allowed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::initialize_sender();
|
||||
|
||||
$executions = 0;
|
||||
|
||||
$next_sync_time = self::$sender->get_next_sync_time( 'full_sync' );
|
||||
|
||||
if ( $next_sync_time ) {
|
||||
$delay = $next_sync_time - time() + 1;
|
||||
if ( $delay > 15 ) {
|
||||
return;
|
||||
} elseif ( $delay > 0 ) {
|
||||
sleep( (int) $delay );
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly only allow 1 do_full_sync call until issue with Immediate Full Sync is resolved.
|
||||
// For more context see p1HpG7-9pe-p2.
|
||||
self::$sender->do_full_sync();
|
||||
++$executions;
|
||||
|
||||
return $executions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -728,7 +793,7 @@ class Actions {
|
||||
if ( $delay > 15 ) {
|
||||
break;
|
||||
} elseif ( $delay > 0 ) {
|
||||
sleep( $delay );
|
||||
sleep( (int) $delay );
|
||||
}
|
||||
}
|
||||
|
||||
@ -857,6 +922,23 @@ class Actions {
|
||||
return $sync_modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Woo's Products sync module to existing modules for sending.
|
||||
*
|
||||
* Note: This module is currently used for WooCommerce Analytics only.
|
||||
*
|
||||
* @param array $sync_modules The list of sync modules declared prior to this filter.
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
*
|
||||
* @return array A list of sync modules that now includes Woo's Products module.
|
||||
*/
|
||||
public static function add_woocommerce_products_sync_module( $sync_modules ) {
|
||||
$sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WooCommerce_Products';
|
||||
return $sync_modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes sync for WP Super Cache.
|
||||
*
|
||||
@ -1127,7 +1209,11 @@ class Actions {
|
||||
delete_option( self::RETRY_AFTER_PREFIX . 'sync' );
|
||||
delete_option( self::RETRY_AFTER_PREFIX . 'full_sync' );
|
||||
// Dedicated sync locks.
|
||||
\Jetpack_Options::delete_raw_option( Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
|
||||
$dedicated_sync_lock_option = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
|
||||
$dedicated_sync_lock_expires_option = $dedicated_sync_lock_option . '_expires';
|
||||
\Jetpack_Options::delete_raw_option( $dedicated_sync_lock_option );
|
||||
\Jetpack_Options::delete_raw_option( $dedicated_sync_lock_expires_option );
|
||||
|
||||
delete_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG );
|
||||
// Lock for disabling Sync sending temporarily.
|
||||
delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
|
||||
@ -1155,7 +1241,7 @@ class Actions {
|
||||
"\n",
|
||||
array_map(
|
||||
function ( $key, $value ) {
|
||||
return wp_json_encode( array( $key => $value ) );
|
||||
return wp_json_encode( array( $key => $value ), JSON_UNESCAPED_SLASHES );
|
||||
},
|
||||
array_keys( (array) $data ),
|
||||
array_values( (array) $data )
|
||||
@ -1184,7 +1270,7 @@ class Actions {
|
||||
}
|
||||
|
||||
if ( $response_code !== 200 || false === isset( $decoded_response['processed_items'] ) ) {
|
||||
if ( is_array( $decoded_response ) && isset( $decoded_response['code'] ) && isset( $decoded_response['message'] ) ) {
|
||||
if ( isset( $decoded_response['code'] ) && isset( $decoded_response['message'] ) ) {
|
||||
return new WP_Error(
|
||||
'jetpack_sync_send_error_' . $decoded_response['code'],
|
||||
$decoded_response['message'],
|
||||
|
||||
@ -30,12 +30,14 @@ class Data_Settings {
|
||||
'get_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
|
||||
'get_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
|
||||
'jetpack_connection_active_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_connection_active_plugins' ),
|
||||
'jetpack_package_versions' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_package_versions' ),
|
||||
'paused_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_plugins' ),
|
||||
'paused_themes' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_paused_themes' ),
|
||||
'timezone' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_timezone' ),
|
||||
'wp_get_environment_type' => 'wp_get_environment_type',
|
||||
'wp_max_upload_size' => 'wp_max_upload_size',
|
||||
'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
|
||||
'jetpack_sync_active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_sync_active_modules' ),
|
||||
),
|
||||
'jetpack_sync_constants_whitelist' => array(
|
||||
'ABSPATH',
|
||||
@ -72,10 +74,6 @@ class Data_Settings {
|
||||
'jetpack_sync_settings_dedicated_sync_enabled',
|
||||
'jetpack_sync_settings_custom_queue_table_enabled',
|
||||
'jetpack_sync_settings_wpcom_rest_api_enabled',
|
||||
/**
|
||||
* Connection related options
|
||||
*/
|
||||
'jetpack_package_versions',
|
||||
/**
|
||||
* Generic site options
|
||||
*/
|
||||
|
||||
@ -43,7 +43,7 @@ class Dedicated_Sender {
|
||||
*
|
||||
* 5 seconds as default value seems sane, but we might want to adjust that in the future.
|
||||
*/
|
||||
const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
|
||||
const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 60;
|
||||
|
||||
/**
|
||||
* The query parameter name to use when passing the current lock id.
|
||||
@ -97,7 +97,7 @@ class Dedicated_Sender {
|
||||
* We want to check it too, to make sure we managed to cover more cases and be more certain we actually
|
||||
* catch calls to the endpoint.
|
||||
*/
|
||||
if ( ! isset( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['rest_route'] ) || ! is_string( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -133,6 +133,10 @@ class Dedicated_Sender {
|
||||
return new WP_Error( 'empty_queue_' . $queue->id );
|
||||
}
|
||||
|
||||
if ( get_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME ) ) {
|
||||
return new WP_Error( 'sender_temporarily_disabled_while_pulling' );
|
||||
}
|
||||
|
||||
// Return early if we've gotten a retry-after header response that is not expired.
|
||||
$retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
|
||||
if ( $retry_time && $retry_time >= microtime( true ) ) {
|
||||
@ -201,41 +205,50 @@ class Dedicated_Sender {
|
||||
* To avoid spawning multiple requests at the same time, we need to have a quick lock that will
|
||||
* allow only a single request to continue if we try to spawn multiple at the same time.
|
||||
*
|
||||
* @return false|mixed|string
|
||||
* @return string|false
|
||||
*/
|
||||
public static function try_lock_spawn_request() {
|
||||
$current_microtime = (string) microtime( true );
|
||||
$option_name = self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
|
||||
$expires_name = $option_name . '_expires';
|
||||
$ttl = self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT;
|
||||
$lock_id = wp_generate_uuid4();
|
||||
$now = microtime( true );
|
||||
|
||||
// Fast path: external object cache is atomic.
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
if ( true !== wp_cache_add( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, 'jetpack', self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
|
||||
// Cache lock has been claimed already.
|
||||
return false;
|
||||
if ( wp_cache_add( $option_name, $lock_id, 'jetpack', $ttl ) ) {
|
||||
return $lock_id;
|
||||
}
|
||||
return false; // Worker already active
|
||||
}
|
||||
|
||||
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
|
||||
global $wpdb;
|
||||
|
||||
if ( ! empty( $current_lock_value ) ) {
|
||||
// Check if time has passed to overwrite the lock - min 5s?
|
||||
if ( is_numeric( $current_lock_value ) && ( ( $current_microtime - $current_lock_value ) < self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
|
||||
// Still in previous lock, quit
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the value is not numeric (float/current time), we want to just overwrite it and continue.
|
||||
// 1) Check & clear expired lock (best effort; failure here is harmless)
|
||||
$expiry = (float) \Jetpack_Options::get_raw_option( $expires_name, 0 );
|
||||
if ( ! $expiry || $expiry < $now ) {
|
||||
// Either missing (edge case) or expired → clean up
|
||||
\Jetpack_Options::delete_raw_option( $option_name );
|
||||
\Jetpack_Options::delete_raw_option( $expires_name );
|
||||
}
|
||||
|
||||
// Update. We don't want it to autoload, as we want to fetch it right before the checks.
|
||||
\Jetpack_Options::update_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, false );
|
||||
// Give some time for the update to happen
|
||||
usleep( wp_rand( 1000, 3000 ) );
|
||||
// 2) Atomic acquisition: INSERT IGNORE (succeeds only if the lock doesn't exist)
|
||||
$inserted = $wpdb->query( // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching --- Ensure atomicity.
|
||||
$wpdb->prepare(
|
||||
"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload )
|
||||
VALUES ( %s, %s, 'no' )",
|
||||
$option_name,
|
||||
maybe_serialize( $lock_id )
|
||||
)
|
||||
);
|
||||
|
||||
$updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
|
||||
|
||||
if ( $updated_value === $current_microtime ) {
|
||||
return $current_microtime;
|
||||
if ( $inserted ) {
|
||||
// 3) We own the lock — store expiry separately
|
||||
\Jetpack_Options::update_raw_option( $expires_name, $now + $ttl, false );
|
||||
return $lock_id; // Success
|
||||
}
|
||||
|
||||
// Lock already present → normal state → do not spawn
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -253,18 +266,24 @@ class Dedicated_Sender {
|
||||
}
|
||||
|
||||
// If it's still not a valid lock_id, throw an error and let the lock process figure it out.
|
||||
if ( empty( $lock_id ) || ! is_numeric( $lock_id ) ) {
|
||||
if ( empty( $lock_id ) ) {
|
||||
return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
|
||||
}
|
||||
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
if ( (string) $lock_id === wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true ) ) {
|
||||
$cached = wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true );
|
||||
if ( (string) $lock_id === $cached ) {
|
||||
wp_cache_delete( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
|
||||
$current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
|
||||
|
||||
if ( (string) $lock_id === $current_lock_value ) {
|
||||
\Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
|
||||
return true;
|
||||
@ -280,7 +299,7 @@ class Dedicated_Sender {
|
||||
*/
|
||||
public static function get_request_lock_id_from_request() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) || ! is_numeric( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
|
||||
if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -397,6 +416,13 @@ class Dedicated_Sender {
|
||||
}
|
||||
}
|
||||
|
||||
$current_setting = Settings::is_dedicated_sync_enabled();
|
||||
|
||||
// No need to update if current setting matches header value.
|
||||
if ( $current_setting === (bool) $dedicated_sync_enabled ) {
|
||||
return $current_setting;
|
||||
}
|
||||
|
||||
Settings::update_settings(
|
||||
array(
|
||||
'dedicated_sync_enabled' => $dedicated_sync_enabled,
|
||||
|
||||
@ -185,6 +185,7 @@ class Defaults {
|
||||
'wp_page_for_privacy_policy',
|
||||
'wpcom_ai_site_prompt',
|
||||
'wpcom_classic_early_release',
|
||||
'wpcom_newsletter_send_default',
|
||||
'wpcom_featured_image_in_email',
|
||||
'jetpack_gravatar_in_email',
|
||||
'jetpack_author_in_email',
|
||||
@ -204,7 +205,6 @@ class Defaults {
|
||||
'jetpack_subscriptions_from_name',
|
||||
'jetpack_verbum_subscription_modal',
|
||||
'jetpack_blocks_disabled',
|
||||
'jetpack_package_versions',
|
||||
'jetpack_newsletters_publishing_default_frequency',
|
||||
'jetpack_scheduled_plugins_update',
|
||||
'jetpack_waf_automatic_rules',
|
||||
@ -357,6 +357,7 @@ class Defaults {
|
||||
'wp_version' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
|
||||
'active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_active_modules' ),
|
||||
'jetpack_connection_active_plugins' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_connection_active_plugins' ),
|
||||
'jetpack_package_versions' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_package_versions' ),
|
||||
'jetpack_sync_active_modules' => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_jetpack_sync_active_modules' ),
|
||||
);
|
||||
|
||||
@ -467,6 +468,8 @@ class Defaults {
|
||||
'secupress_log_err404', // SecuPress Plugin - Log 404 pages
|
||||
'iw_omnibus_price_log', // Omnibus Plugin - Log price changes.
|
||||
'od_url_metrics', // Optimization Detective - Log URL metrics.
|
||||
'ap_outbox', // ActivityPub Outbox; only used for broadcasting ActivityPub activity to followers.
|
||||
'shop_order_placehold', // WooCommerce placeholder - Used to maintain compatibility and references when switching between WP Posts-based order storage and the newer HPOS tables.
|
||||
);
|
||||
|
||||
/**
|
||||
@ -773,6 +776,8 @@ class Defaults {
|
||||
'_wp_page_template',
|
||||
'_wp_trash_meta_comments_status',
|
||||
'_wpas_feature_enabled',
|
||||
'_wpas_connection_overrides',
|
||||
'_wpas_customize_per_network',
|
||||
'_wpas_mess',
|
||||
'_wpas_options',
|
||||
'advanced_seo_description', // Jetpack_SEO_Posts::DESCRIPTION_META_KEY.
|
||||
@ -820,6 +825,7 @@ class Defaults {
|
||||
'hc_foreign_user_id',
|
||||
'hc_post_as',
|
||||
'hc_wpcom_id_sig',
|
||||
'protocol',
|
||||
);
|
||||
|
||||
/**
|
||||
@ -841,6 +847,32 @@ class Defaults {
|
||||
return apply_filters( 'jetpack_sync_comment_meta_whitelist', self::$comment_meta_whitelist );
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment types whitelist.
|
||||
*
|
||||
* @var array Comment types that are synced.
|
||||
*/
|
||||
public static $comment_types_whitelist = array( '', 'comment', 'trackback', 'pingback', 'review', 'note' );
|
||||
|
||||
/**
|
||||
* Get the comment types whitelist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_comment_types_whitelist() {
|
||||
/**
|
||||
* Comment types present in this list will be synced to WordPress.com.
|
||||
*
|
||||
* @module sync
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 7.6.0
|
||||
*
|
||||
* @param array A list of comment types.
|
||||
*/
|
||||
return apply_filters( 'jetpack_sync_whitelisted_comment_types', self::$comment_types_whitelist );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default theme support whitelist.
|
||||
*
|
||||
@ -1185,6 +1217,13 @@ class Defaults {
|
||||
*/
|
||||
public static $default_comment_meta_whitelist = array();
|
||||
|
||||
/**
|
||||
* Default for sync actions blacklist.
|
||||
*
|
||||
* @var array Empty array.
|
||||
*/
|
||||
public static $default_sync_actions_blacklist = array();
|
||||
|
||||
/**
|
||||
* Default for disabling sync across the site.
|
||||
*
|
||||
@ -1329,7 +1368,7 @@ class Defaults {
|
||||
'max_chunks' => 10,
|
||||
),
|
||||
'posts' => array(
|
||||
'chunk_size' => 100,
|
||||
'chunk_size' => 500,
|
||||
'max_chunks' => 1,
|
||||
),
|
||||
'term_relationships' => array(
|
||||
@ -1371,7 +1410,16 @@ class Defaults {
|
||||
/**
|
||||
* Default for enabling wpcom rest api for Sync.
|
||||
*
|
||||
* @var int Bool-ish. Default 0.
|
||||
* @var int Bool-ish. Default 1.
|
||||
*/
|
||||
public static $default_wpcom_rest_api_enabled = 0;
|
||||
public static $default_wpcom_rest_api_enabled = 1;
|
||||
|
||||
/**
|
||||
* A list of 'jetpack_options' specific keys we want to ignore.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $jetpack_options_blacklist = array(
|
||||
'last_heartbeat',
|
||||
);
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ class Functions {
|
||||
public static function sanitize_taxonomy( $taxonomy ) {
|
||||
|
||||
// Lets clone the taxonomy object instead of modifing the global one.
|
||||
$cloned_taxonomy = json_decode( wp_json_encode( $taxonomy ) );
|
||||
$cloned_taxonomy = json_decode( wp_json_encode( $taxonomy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
|
||||
|
||||
// recursive taxonomies are no fun.
|
||||
if ( $cloned_taxonomy === null ) {
|
||||
@ -146,7 +146,7 @@ class Functions {
|
||||
$post_type_object->add_rewrite_rules();
|
||||
$post_type_object->add_hooks();
|
||||
$post_type_object->register_taxonomies();
|
||||
return (object) $post_type_object;
|
||||
return $post_type_object;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -534,7 +534,7 @@ class Functions {
|
||||
$formatted_gmt_offset = str_replace(
|
||||
array( '.25', '.5', '.75' ),
|
||||
array( ':15', ':30', ':45' ),
|
||||
(string) $formatted_gmt_offset
|
||||
$formatted_gmt_offset
|
||||
);
|
||||
|
||||
/* translators: %s is UTC offset, e.g. "+1" */
|
||||
@ -604,7 +604,19 @@ class Functions {
|
||||
*/
|
||||
public static function json_wrap( &$any, $seen_nodes = array() ) {
|
||||
if ( is_object( $any ) ) {
|
||||
$input = get_object_vars( $any );
|
||||
$input = get_object_vars( $any );
|
||||
|
||||
// WordPress 6.9 introduced lazy-loading of WP_User `roles`, `caps`, and `allcaps` properties.
|
||||
// It also made said properties protected, so we need to access them and set them as keys manually.
|
||||
if ( $any instanceof \WP_User ) {
|
||||
$roles = $any->roles;
|
||||
$caps = $any->caps;
|
||||
$allcaps = $any->allcaps;
|
||||
$input['roles'] = $roles;
|
||||
$input['caps'] = $caps;
|
||||
$input['allcaps'] = $allcaps;
|
||||
}
|
||||
|
||||
$input['__o'] = 1;
|
||||
} else {
|
||||
$input = &$any;
|
||||
@ -737,4 +749,15 @@ class Functions {
|
||||
$modules = array_map( 'wp_normalize_path', $modules );
|
||||
return $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of Jetpack package versions.
|
||||
*
|
||||
* @since 4.11.1
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_jetpack_package_versions() {
|
||||
return apply_filters( 'jetpack_package_versions', array() );
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,6 +174,24 @@ class Health {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sync queue is healthy based on size and lag.
|
||||
*
|
||||
* The queue is considered healthy if either size OR lag is within its limit.
|
||||
* This matches the logic in Listener::can_add_to_queue(): data loss only occurs
|
||||
* when both limits are exceeded simultaneously.
|
||||
*
|
||||
* @param int $queue_size Current queue size.
|
||||
* @param float $queue_lag Current queue lag in seconds.
|
||||
* @param int $max_queue_size Maximum allowed queue size.
|
||||
* @param float $max_queue_lag Maximum allowed queue lag in seconds.
|
||||
* @return bool Whether the queue is healthy.
|
||||
*/
|
||||
public static function is_queue_healthy( $queue_size, $queue_lag, $max_queue_size, $max_queue_lag ) {
|
||||
return ( $queue_size < $max_queue_size )
|
||||
|| ( $queue_lag < $max_queue_lag );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Sync Status if Full Sync ended of Posts
|
||||
*
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses gzip's DEFLATE
|
||||
* algorithm to compress objects serialized using json_encode
|
||||
@ -57,7 +61,7 @@ class JSON_Deflate_Array_Codec implements Codec_Interface {
|
||||
* @return false|string
|
||||
*/
|
||||
protected function json_serialize( $any ) {
|
||||
return wp_json_encode( Functions::json_wrap( $any ) );
|
||||
return wp_json_encode( Functions::json_wrap( $any ), JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -15,8 +15,9 @@ use Automattic\Jetpack\Roles;
|
||||
* This class monitors actions and logs them to the queue to be sent.
|
||||
*/
|
||||
class Listener {
|
||||
const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state';
|
||||
const QUEUE_STATE_CHECK_TIMEOUT = 30; // 30 seconds.
|
||||
const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state';
|
||||
const QUEUE_STATE_CHECK_TIMEOUT = 30; // 30 seconds.
|
||||
const REQUEST_STATE_CACHE_INVALIDATE_AFTER = 50; // Number of queue adds per request before invalidating the cache.
|
||||
|
||||
/**
|
||||
* Sync queue.
|
||||
@ -46,6 +47,23 @@ class Listener {
|
||||
*/
|
||||
private $sync_queue_lag_limit;
|
||||
|
||||
/**
|
||||
* Per-request in-memory cache of queue state.
|
||||
* Avoids repeated get_transient() calls for every
|
||||
* can_add_to_queue check within a single request.
|
||||
*
|
||||
* @var array<string, array{int, float}>
|
||||
*/
|
||||
private $request_queue_state_cache = array();
|
||||
|
||||
/**
|
||||
* Count of successful adds per queue within this request, used to decide when
|
||||
* to invalidate the in-memory cache so the size estimate stays accurate.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private $request_adds_count = array();
|
||||
|
||||
/**
|
||||
* Singleton implementation.
|
||||
*
|
||||
@ -92,9 +110,6 @@ class Listener {
|
||||
add_action( 'jetpack_activate_module', $handler );
|
||||
add_action( 'jetpack_deactivate_module', $handler );
|
||||
|
||||
// Jetpack Upgrade.
|
||||
add_action( 'updating_jetpack_version', $handler, 10, 2 );
|
||||
|
||||
// Send periodic checksum.
|
||||
add_action( 'jetpack_sync_checksum', $handler );
|
||||
}
|
||||
@ -151,6 +166,8 @@ class Listener {
|
||||
public function force_recheck_queue_limit() {
|
||||
delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->sync_queue->id );
|
||||
delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->full_sync_queue->id );
|
||||
$this->request_queue_state_cache = array();
|
||||
$this->request_adds_count = array();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -167,6 +184,18 @@ class Listener {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Per-request in-memory cache: avoids a get_transient() call on every can_add_to_queue
|
||||
// check within the same request when many actions are enqueued.
|
||||
if ( isset( $this->request_queue_state_cache[ $queue->id ] ) ) {
|
||||
list( $queue_size, $queue_age ) = $this->request_queue_state_cache[ $queue->id ];
|
||||
return Health::is_queue_healthy(
|
||||
$queue_size + ( $this->request_adds_count[ $queue->id ] ?? 0 ) + 1,
|
||||
$queue_age,
|
||||
$this->sync_queue_size_limit,
|
||||
$this->sync_queue_lag_limit
|
||||
);
|
||||
}
|
||||
|
||||
$state_transient_name = self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $queue->id;
|
||||
|
||||
$queue_state = get_transient( $state_transient_name );
|
||||
@ -176,11 +205,18 @@ class Listener {
|
||||
set_transient( $state_transient_name, $queue_state, self::QUEUE_STATE_CHECK_TIMEOUT );
|
||||
}
|
||||
|
||||
// Populate in-memory cache from the transient result so subsequent calls this request
|
||||
// don't need to hit the object cache or DB at all.
|
||||
$this->request_queue_state_cache[ $queue->id ] = $queue_state;
|
||||
|
||||
list( $queue_size, $queue_age ) = $queue_state;
|
||||
|
||||
return ( $queue_age < $this->sync_queue_lag_limit )
|
||||
||
|
||||
( ( $queue_size + 1 ) < $this->sync_queue_size_limit );
|
||||
return Health::is_queue_healthy(
|
||||
$queue_size + 1,
|
||||
$queue_age,
|
||||
$this->sync_queue_size_limit,
|
||||
$this->sync_queue_lag_limit
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -280,6 +316,21 @@ class Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip enqueueing any Sync action when triggered by Jetpack CRM Woo Sync background job to avoid periodic noise.
|
||||
if (
|
||||
( function_exists( 'doing_action' ) && doing_action( 'jpcrm_woosync_sync' ) )
|
||||
|| defined( 'jpcrm_woosync_running' )
|
||||
|| defined( 'jpcrm_woosync_cron_running' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip enqueueing if current action is blacklisted.
|
||||
$sync_actions_blacklist = Settings::get_setting( 'sync_actions_blacklist' );
|
||||
if ( is_array( $sync_actions_blacklist ) && in_array( $current_filter, $sync_actions_blacklist, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action hook to execute when anything on the whitelist gets sent to the queue to sync.
|
||||
*
|
||||
@ -311,7 +362,7 @@ class Listener {
|
||||
*/
|
||||
if ( ! $this->can_add_to_queue( $queue ) ) {
|
||||
if ( 'sync' === $queue->id ) {
|
||||
$this->sync_data_loss( $queue );
|
||||
$this->sync_data_loss( $queue, $current_filter );
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -359,6 +410,16 @@ class Listener {
|
||||
);
|
||||
}
|
||||
|
||||
// Track how many items have been added to this queue during this request and periodically
|
||||
// invalidate the in-memory queue-state cache so the size/lag estimate stays accurate.
|
||||
// Without this, a burst that fills the queue mid-request would keep passing can_add_to_queue
|
||||
// with a stale "queue is fine" result captured at the start of the request.
|
||||
$this->request_adds_count[ $queue->id ] = ( $this->request_adds_count[ $queue->id ] ?? 0 ) + 1;
|
||||
if ( $this->request_adds_count[ $queue->id ] >= self::REQUEST_STATE_CACHE_INVALIDATE_AFTER ) {
|
||||
unset( $this->request_queue_state_cache[ $queue->id ] );
|
||||
$this->request_adds_count[ $queue->id ] = 0;
|
||||
}
|
||||
|
||||
// since we've added some items, let's try to load the sender so we can send them as quickly as possible.
|
||||
if ( ! Actions::$sender ) {
|
||||
add_filter( 'jetpack_sync_sender_should_load', __NAMESPACE__ . '\Actions::should_initialize_sender_enqueue', 10, 1 );
|
||||
@ -371,10 +432,13 @@ class Listener {
|
||||
/**
|
||||
* Sync Data Loss Handler
|
||||
*
|
||||
* @param Queue $queue Sync queue.
|
||||
* Sends a single 'jetpack_sync_data_loss' action to WP.com with timestamp, queue_size, queue_lag and current_filter.
|
||||
*
|
||||
* @param Queue $queue Sync queue.
|
||||
* @param string $current_filter Name of action that triggered sync.
|
||||
* @return boolean was send successful
|
||||
*/
|
||||
public function sync_data_loss( $queue ) {
|
||||
public function sync_data_loss( $queue, $current_filter = '' ) {
|
||||
if ( ! Settings::is_sync_enabled() ) {
|
||||
return;
|
||||
}
|
||||
@ -388,6 +452,9 @@ class Listener {
|
||||
'timestamp' => microtime( true ),
|
||||
'queue_size' => $queue->size(),
|
||||
'queue_lag' => $queue->lag(),
|
||||
'extra' => array(
|
||||
'current_filter' => $current_filter,
|
||||
),
|
||||
);
|
||||
|
||||
$sender = Sender::get_instance();
|
||||
@ -432,7 +499,56 @@ class Listener {
|
||||
$ip = IP_Utils::get_ip();
|
||||
|
||||
$actor['ip'] = $ip ? $ip : '';
|
||||
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'unknown';
|
||||
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'unknown';
|
||||
}
|
||||
|
||||
$raw_mcp_header = '';
|
||||
if ( isset( $_SERVER['HTTP_X_WPCOM_MCP'] ) && is_string( $_SERVER['HTTP_X_WPCOM_MCP'] ) ) {
|
||||
$raw_mcp_header = trim( wp_unslash( $_SERVER['HTTP_X_WPCOM_MCP'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization happens below.
|
||||
}
|
||||
|
||||
if ( ! empty( $raw_mcp_header ) && preg_match( '/^[A-Za-z0-9+\/=]+$/', $raw_mcp_header ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding MCP header payload.
|
||||
$decoded = base64_decode( $raw_mcp_header, true );
|
||||
if ( false !== $decoded ) {
|
||||
$mcp_data = json_decode( $decoded, true );
|
||||
if ( is_array( $mcp_data ) ) {
|
||||
if ( isset( $mcp_data['mcp_client_name'] ) && is_string( $mcp_data['mcp_client_name'] ) ) {
|
||||
$actor['mcp_client_name'] = sanitize_text_field( $mcp_data['mcp_client_name'] );
|
||||
}
|
||||
if ( isset( $mcp_data['mcp_client_version'] ) && is_string( $mcp_data['mcp_client_version'] ) ) {
|
||||
$actor['mcp_client_version'] = sanitize_text_field( $mcp_data['mcp_client_version'] );
|
||||
}
|
||||
if ( ! empty( $actor['mcp_client_name'] ) || ! empty( $actor['mcp_client_version'] ) ) {
|
||||
$actor['is_mcp_agent'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the actor data attached to sync events.
|
||||
*
|
||||
* Actor data identifies who or what triggered a sync event (user info,
|
||||
* request context, MCP client details, etc.) and is sent alongside every
|
||||
* event to WordPress.com.
|
||||
*
|
||||
* @since 4.33.0
|
||||
*
|
||||
* @param array $actor Associative array of actor information.
|
||||
*/
|
||||
$actor = apply_filters( 'jetpack_sync_actor_data', $actor );
|
||||
|
||||
// Ensure the filter returns a valid array.
|
||||
if ( ! is_array( $actor ) ) {
|
||||
$actor = array();
|
||||
}
|
||||
|
||||
// Sanitize string values added via the filter.
|
||||
foreach ( $actor as $key => $value ) {
|
||||
if ( is_string( $value ) ) {
|
||||
$actor[ $key ] = sanitize_text_field( $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $actor;
|
||||
|
||||
@ -12,7 +12,7 @@ namespace Automattic\Jetpack\Sync;
|
||||
*/
|
||||
class Package_Version {
|
||||
|
||||
const PACKAGE_VERSION = '4.9.2';
|
||||
const PACKAGE_VERSION = '4.35.0';
|
||||
|
||||
const PACKAGE_SLUG = 'sync';
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ class Queue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the front-most item on the queue without checking it out.
|
||||
* Get the front-most items on the queue without checking them out.
|
||||
*
|
||||
* @param int $count Number of items to return when looking at the items.
|
||||
*
|
||||
@ -136,6 +136,22 @@ class Queue {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last-added items on the queue without checking them out.
|
||||
*
|
||||
* @param int $count Number of items to return when looking at the items.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function peek_newest( $count = 1 ) {
|
||||
$items = $this->fetch_items( $count, 'DESC' );
|
||||
if ( $items ) {
|
||||
return Utils::get_item_values( $items );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets items with particular IDs.
|
||||
*
|
||||
@ -245,15 +261,40 @@ class Queue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop elements from the queue.
|
||||
* Remove the oldest items from the queue.
|
||||
*
|
||||
* @param int $limit Number of items to pop from the queue.
|
||||
* @param int $limit Number of items to remove from the queue.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function pop( $limit ) {
|
||||
$items = $this->fetch_items( $limit );
|
||||
|
||||
if ( ! $items ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$ids = $this->get_ids( $items );
|
||||
|
||||
$this->delete( $ids );
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the newest items from the queue.
|
||||
*
|
||||
* @param int $limit Number of items to remove from the queue.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function pop_newest( $limit ) {
|
||||
$items = $this->fetch_items( $limit, 'DESC' );
|
||||
|
||||
if ( ! $items ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$ids = $this->get_ids( $items );
|
||||
|
||||
$this->delete( $ids );
|
||||
@ -329,9 +370,8 @@ class Queue {
|
||||
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
|
||||
while ( ( $current_item = array_shift( $current_items ) ) !== null ) {
|
||||
// @codingStandardsIgnoreStart
|
||||
$current_item->value = unserialize( $current_item->value );
|
||||
$current_item->value = @unserialize( $current_item->value );
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
||||
$items[] = $current_item;
|
||||
}
|
||||
}
|
||||
@ -610,11 +650,15 @@ class Queue {
|
||||
* Return the items in the queue.
|
||||
*
|
||||
* @param null|int $limit Limit to the number of items we fetch at once.
|
||||
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
|
||||
* Any other value will be treated as 'ASC'.
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
private function fetch_items( $limit = null ) {
|
||||
$items = $this->queue_storage->fetch_items( $limit );
|
||||
private function fetch_items( $limit = null, $order = 'ASC' ) {
|
||||
$order = 'DESC' === $order ? 'DESC' : 'ASC';
|
||||
|
||||
$items = $this->queue_storage->fetch_items( $limit, $order );
|
||||
|
||||
return $this->unserialize_values( $items );
|
||||
}
|
||||
|
||||
@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Replicastore\Table_Checksum_Users;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of Replicastore Interface which returns data stored in a WordPress.org DB.
|
||||
* This is useful to compare values in the local WP DB to values in the synced replica store
|
||||
@ -869,6 +873,7 @@ class Replicastore implements Replicastore_Interface {
|
||||
if ( ! $t || is_wp_error( $t ) ) {
|
||||
return $t;
|
||||
}
|
||||
// @phan-suppress-next-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
|
||||
return get_terms( $taxonomy );
|
||||
}
|
||||
|
||||
@ -1289,7 +1294,7 @@ class Replicastore implements Replicastore_Interface {
|
||||
try {
|
||||
$range_edges = $checksum_table->get_range_edges( $start_id, $end_id );
|
||||
} catch ( Exception $ex ) {
|
||||
return new WP_Error( 'invalid_range_edges', '[' . $start_id . '-' . $end_id . ']: ' . $ex->getMessage() );
|
||||
return new WP_Error( 'invalid_range_edges', '[' . ( $start_id ?? 'null' ) . '-' . ( $end_id ?? 'null' ) . ']: ' . $ex->getMessage() );
|
||||
}
|
||||
|
||||
if ( $only_range_edges ) {
|
||||
|
||||
@ -77,7 +77,7 @@ class REST_Endpoints {
|
||||
'permission_callback' => __CLASS__ . '::verify_default_permissions',
|
||||
'args' => array(
|
||||
'fields' => array(
|
||||
'description' => __( 'Comma seperated list of additional fields that should be included in status.', 'jetpack-sync' ),
|
||||
'description' => __( 'Comma-separated list of additional fields that should be included in status.', 'jetpack-sync' ),
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
),
|
||||
@ -175,6 +175,43 @@ class REST_Endpoints {
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::checkout',
|
||||
'permission_callback' => __CLASS__ . '::verify_default_permissions',
|
||||
'args' => array(
|
||||
'queue' => array(
|
||||
'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
|
||||
'type' => 'string',
|
||||
'required' => false,
|
||||
),
|
||||
'number_of_items' => array(
|
||||
'description' => __( 'Number of items to checkout from the queue.', 'jetpack-sync' ),
|
||||
'type' => 'integer',
|
||||
'required' => false,
|
||||
),
|
||||
'pop' => array(
|
||||
'description' => __( 'Pop items from the queue instead of checking out.', 'jetpack-sync' ),
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
'force' => array(
|
||||
'description' => __( 'Force unlock the queue before checkout.', 'jetpack-sync' ),
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
'encode' => array(
|
||||
'description' => __( 'Encode the items before sending.', 'jetpack-sync' ),
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'default' => true,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
'use_memory_limit' => array(
|
||||
'description' => __( 'Use memory-based checkout instead of fixed item count.', 'jetpack-sync' ),
|
||||
'type' => 'boolean',
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@ -332,6 +369,17 @@ class REST_Endpoints {
|
||||
'permission_callback' => __CLASS__ . '::verify_default_permissions',
|
||||
)
|
||||
);
|
||||
|
||||
// Clear Sync queue.
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/sync/clear-queue',
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => __CLASS__ . '::clear_queue',
|
||||
'permission_callback' => __CLASS__ . '::verify_default_permissions',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -470,21 +518,53 @@ class REST_Endpoints {
|
||||
/**
|
||||
* Update Sync health.
|
||||
*
|
||||
* IN_SYNC is only set if the incremental queue is within size and lag limits.
|
||||
*
|
||||
* @since 1.23.1
|
||||
*
|
||||
* @param \WP_REST_Request $request The request sent to the WP REST API.
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function sync_health( $request ) {
|
||||
$requested_status = $request->get_param( 'status' );
|
||||
|
||||
switch ( $request->get_param( 'status' ) ) {
|
||||
switch ( $requested_status ) {
|
||||
case Health::STATUS_IN_SYNC:
|
||||
// Only allow setting IN_SYNC if the incremental queue is healthy.
|
||||
$sync_queue = Listener::get_instance()->get_sync_queue();
|
||||
$queue_size = $sync_queue->size();
|
||||
$queue_lag = $sync_queue->lag();
|
||||
$queue_healthy = Health::is_queue_healthy(
|
||||
$queue_size,
|
||||
$queue_lag,
|
||||
Settings::get_setting( 'max_queue_size' ),
|
||||
Settings::get_setting( 'max_queue_lag' )
|
||||
);
|
||||
if ( ! $queue_healthy ) {
|
||||
Health::update_status( Health::STATUS_OUT_OF_SYNC );
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => Health::get_status(),
|
||||
'message' => 'Sync queue is not healthy (size and lag over limit). Status not set to in_sync.',
|
||||
'queue_size' => $queue_size,
|
||||
'queue_lag' => $queue_lag,
|
||||
)
|
||||
);
|
||||
}
|
||||
Health::update_status( $requested_status );
|
||||
break;
|
||||
case Health::STATUS_OUT_OF_SYNC:
|
||||
Health::update_status( $request->get_param( 'status' ) );
|
||||
Health::update_status( $requested_status );
|
||||
break;
|
||||
default:
|
||||
return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' );
|
||||
return new WP_Error(
|
||||
'invalid_status',
|
||||
'Invalid Sync Status Provided.',
|
||||
array(
|
||||
'status' => 400,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// re-fetch so we see what's really being stored.
|
||||
@ -617,9 +697,16 @@ class REST_Endpoints {
|
||||
return $queue_name;
|
||||
}
|
||||
|
||||
$number_of_items = $args['number_of_items'];
|
||||
if ( $number_of_items < 1 || $number_of_items > 100 ) {
|
||||
return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and less then 100', 400 );
|
||||
$use_memory_limit = ! empty( $args['use_memory_limit'] );
|
||||
|
||||
if ( $use_memory_limit && ! empty( $args['pop'] ) ) {
|
||||
return new WP_Error( 'invalid_args', 'pop cannot be used with use_memory_limit', 400 );
|
||||
}
|
||||
|
||||
if ( ! $use_memory_limit ) {
|
||||
if ( empty( $args['number_of_items'] ) || $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) {
|
||||
return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and up to 100', 400 );
|
||||
}
|
||||
}
|
||||
|
||||
// REST Sender.
|
||||
@ -629,7 +716,8 @@ class REST_Endpoints {
|
||||
return rest_ensure_response( $sender->immediate_full_sync_pull() );
|
||||
}
|
||||
|
||||
$response = $sender->queue_pull( $queue_name, $number_of_items, $args );
|
||||
$number_of_items = $use_memory_limit ? null : $args['number_of_items'];
|
||||
$response = $sender->queue_pull( $queue_name, $number_of_items, $args );
|
||||
// Disable sending while pulling.
|
||||
if ( ! is_wp_error( $response ) ) {
|
||||
set_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME, time(), Sender::TEMP_SYNC_DISABLE_TRANSIENT_EXPIRY );
|
||||
@ -810,6 +898,27 @@ class REST_Endpoints {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the Sync queue.
|
||||
*
|
||||
* @since 4.30.0
|
||||
*
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public static function clear_queue() {
|
||||
$queue = new Queue( 'sync' );
|
||||
$queue->reset();
|
||||
|
||||
// Re-enable sending in case it was temporarily disabled during a pull.
|
||||
delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that request has default permissions to perform sync actions.
|
||||
*
|
||||
|
||||
@ -26,9 +26,9 @@ class REST_Sender {
|
||||
/**
|
||||
* Checkout objects from the queue
|
||||
*
|
||||
* @param string $queue_name Name of Queue.
|
||||
* @param int $number_of_items Number of Items.
|
||||
* @param array $args arguments.
|
||||
* @param string $queue_name Name of Queue.
|
||||
* @param int|null $number_of_items Number of Items. Null for memory-based checkout.
|
||||
* @param array $args Request arguments. Supports 'pop', 'force', 'encode', and 'use_memory_limit'.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
@ -117,22 +117,30 @@ class REST_Sender {
|
||||
/**
|
||||
* Checkout items out of the sync queue.
|
||||
*
|
||||
* @param Queue $queue Sync Queue.
|
||||
* @param int $number_of_items Number of items to checkout.
|
||||
* @param Queue $queue Sync Queue.
|
||||
* @param int|null $number_of_items Number of items to checkout. When null, uses memory-based checkout with default settings.
|
||||
*
|
||||
* @return WP_Error
|
||||
* @return Queue_Buffer|WP_Error
|
||||
*/
|
||||
protected function get_buffer( $queue, $number_of_items ) {
|
||||
$start = time();
|
||||
$max_duration = 5; // this will try to get the buffer.
|
||||
|
||||
$buffer = $queue->checkout( $number_of_items );
|
||||
$use_memory_limit = null === $number_of_items;
|
||||
$memory_limit = $use_memory_limit ? Settings::get_setting( 'dequeue_max_bytes' ) : null;
|
||||
$max_rows = $use_memory_limit ? Settings::get_setting( 'upload_max_rows' ) : null;
|
||||
|
||||
$buffer = $use_memory_limit
|
||||
? $queue->checkout_with_memory_limit( $memory_limit, $max_rows )
|
||||
: $queue->checkout( $number_of_items );
|
||||
$duration = time() - $start;
|
||||
|
||||
while ( is_wp_error( $buffer ) && $duration < $max_duration ) {
|
||||
sleep( 2 );
|
||||
$duration = time() - $start;
|
||||
$buffer = $queue->checkout( $number_of_items );
|
||||
$buffer = $use_memory_limit
|
||||
? $queue->checkout_with_memory_limit( $memory_limit, $max_rows )
|
||||
: $queue->checkout( $number_of_items );
|
||||
}
|
||||
|
||||
if ( false === $buffer ) {
|
||||
|
||||
@ -358,7 +358,10 @@ class Sender {
|
||||
* @return boolean|WP_Error True if this sync sending was successful, error object otherwise.
|
||||
*/
|
||||
public function do_sync() {
|
||||
if ( ! Settings::is_dedicated_sync_enabled() ) {
|
||||
// Sync directly during cron. We are doing this because otherwise
|
||||
// the dedicated sync flow would be spawning HTTP requests during cron shutdown,
|
||||
// which can be unreliable and cause sync lag for time-sensitive events like updates.
|
||||
if ( ! Settings::is_dedicated_sync_enabled() || Settings::is_doing_cron() ) {
|
||||
$result = $this->do_sync_and_set_delays( $this->sync_queue );
|
||||
} else {
|
||||
$result = Dedicated_Sender::spawn_sync( $this->sync_queue );
|
||||
@ -418,12 +421,12 @@ class Sender {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
// Output not used right now. Try to release dedicated sync lock
|
||||
Dedicated_Sender::try_release_lock_spawn_request();
|
||||
|
||||
// Actually try to send Sync events.
|
||||
$result = $this->do_sync_and_set_delays( $this->sync_queue );
|
||||
|
||||
// Output not used right now. Try to release dedicated sync lock
|
||||
Dedicated_Sender::try_release_lock_spawn_request();
|
||||
|
||||
// If no errors occurred, re-spawn a dedicated Sync request.
|
||||
if ( true === $result ) {
|
||||
Dedicated_Sender::spawn_sync( $this->sync_queue );
|
||||
@ -497,8 +500,10 @@ class Sender {
|
||||
if ( 'wpcom_error' === $sync_result->get_error_code() ) {
|
||||
$this->set_next_sync_time( time() + self::WPCOM_ERROR_SYNC_DELAY, $queue->id );
|
||||
}
|
||||
} elseif ( $exceeded_sync_wait_threshold ) {
|
||||
// If we actually sent data and it took a while, wait before sending again.
|
||||
} elseif ( $exceeded_sync_wait_threshold && ! Settings::is_doing_cron() ) {
|
||||
// If a send was slow, briefly pause before the next one.
|
||||
// Applies only to Dedicated/Normal Sync to avoid impacting user traffic;
|
||||
// cron jobs are exempt.
|
||||
$this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id );
|
||||
}
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ class Server {
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
*
|
||||
* @param token The token object of the misbehaving site
|
||||
* @param object|null $token The token object of the misbehaving site
|
||||
*/
|
||||
do_action( 'jetpack_sync_multi_request_fail', $token );
|
||||
|
||||
@ -155,7 +155,8 @@ class Server {
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
*
|
||||
* @param array Array of actions received from the remote site
|
||||
* @param array $events Array of actions received from the remote site
|
||||
* @param object|null $token The auth token used to invoke the API
|
||||
*/
|
||||
do_action( 'jetpack_sync_remote_actions', $events, $token );
|
||||
|
||||
@ -168,14 +169,14 @@ class Server {
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
*
|
||||
* @param string $action_name The name of the action executed on the remote site
|
||||
* @param array $args The arguments passed to the action
|
||||
* @param int $user_id The external_user_id who did the action
|
||||
* @param bool $silent Whether the item was created via import
|
||||
* @param double $timestamp Timestamp (in seconds) when the action occurred
|
||||
* @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted
|
||||
* @param string $queue_id ID of the queue from which the event was sent (sync or full_sync)
|
||||
* @param array $token The auth token used to invoke the API
|
||||
* @param string $action_name The name of the action executed on the remote site
|
||||
* @param array $args The arguments passed to the action
|
||||
* @param int $user_id The external_user_id who did the action
|
||||
* @param bool $silent Whether the item was created via import
|
||||
* @param double $timestamp Timestamp (in seconds) when the action occurred
|
||||
* @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted
|
||||
* @param string $queue_id ID of the queue from which the event was sent (sync or full_sync)
|
||||
* @param object|null $token The auth token used to invoke the API
|
||||
*/
|
||||
do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ class Settings {
|
||||
'dedicated_sync_enabled' => true,
|
||||
'custom_queue_table_enabled' => true,
|
||||
'wpcom_rest_api_enabled' => true,
|
||||
'sync_actions_blacklist' => true,
|
||||
);
|
||||
|
||||
/**
|
||||
@ -283,6 +284,7 @@ class Settings {
|
||||
/**
|
||||
* Regular option update and handling
|
||||
*/
|
||||
$updated = false;
|
||||
if ( self::is_network_setting( $setting ) ) {
|
||||
if ( is_multisite() && is_main_site() ) {
|
||||
$updated = update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value );
|
||||
@ -302,26 +304,21 @@ class Settings {
|
||||
if ( 'dedicated_sync_enabled' === $setting && $updated && (bool) $value ) {
|
||||
if ( ! Dedicated_Sender::can_spawn_dedicated_sync_request() ) {
|
||||
update_option( self::SETTINGS_OPTION_PREFIX . $setting, 0, true );
|
||||
}
|
||||
}
|
||||
$listener = Listener::get_instance();
|
||||
// Remove the last two actions from the queue since we failed to enable Dedicated Sync.
|
||||
// Those would be `updated_option` with `jetpack_sync_settings_dedicated_sync_enabled` set to 1 and then 0 again.
|
||||
$queue = $listener->get_sync_queue();
|
||||
$items = $queue->peek_newest( 2 );
|
||||
$key = 'jetpack_sync_settings_dedicated_sync_enabled';
|
||||
|
||||
// Do not enable wpcom rest api if we cannot send a test request.
|
||||
|
||||
if ( 'wpcom_rest_api_enabled' === $setting && $updated && (bool) $value ) {
|
||||
$sender = Sender::get_instance();
|
||||
$data = array(
|
||||
'timestamp' => microtime( true ),
|
||||
);
|
||||
$items = $sender->send_action( 'jetpack_sync_wpcom_rest_api_enable_test', $data );
|
||||
// If we can't send a test request, disable the setting and send action tolog the error.
|
||||
if ( is_wp_error( $items ) ) {
|
||||
update_option( self::SETTINGS_OPTION_PREFIX . $setting, 0, true );
|
||||
$data = array(
|
||||
'timestamp' => microtime( true ),
|
||||
'response_code' => $items->get_error_code(),
|
||||
'response_body' => $items->get_error_message() ?? '',
|
||||
);
|
||||
$sender->send_action( 'jetpack_sync_wpcom_rest_api_enable_error', $data );
|
||||
if (
|
||||
isset( $items[0][1][0] )
|
||||
&& isset( $items[1][1][0] )
|
||||
&& $items[0][1][0] === $key
|
||||
&& $items[1][1][0] === $key
|
||||
) {
|
||||
$queue->pop_newest( 2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -523,6 +520,23 @@ class Settings {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns structured filter values for allowed comment types.
|
||||
*
|
||||
* @access public
|
||||
* @static
|
||||
*
|
||||
* @return array Comment type filter values.
|
||||
*/
|
||||
public static function get_allowed_comment_types_structured() {
|
||||
return array(
|
||||
'comment_type' => array(
|
||||
'operator' => 'IN',
|
||||
'values' => array_map( 'esc_sql', Defaults::get_comment_types_whitelist() ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns escaped SQL for comments, excluding any spam comments.
|
||||
* Can be injected directly into a WHERE clause.
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of Automattic\Jetpack\Sync\Codec_Interface that uses base64
|
||||
* algorithm to compress objects serialized using json_encode.
|
||||
|
||||
@ -54,7 +54,9 @@ class Users {
|
||||
public static function user_role_change( $user_id ) {
|
||||
$connection = new Jetpack_Connection();
|
||||
if ( $connection->is_user_connected( $user_id ) ) {
|
||||
self::get_role( $user_id );
|
||||
self::update_role_on_com( $user_id );
|
||||
|
||||
// Try to choose a new master if we're demoting the current one.
|
||||
self::maybe_demote_master_user( $user_id );
|
||||
}
|
||||
@ -66,11 +68,12 @@ class Users {
|
||||
* @access public
|
||||
* @static
|
||||
*
|
||||
* @param int $user_id ID of the user.
|
||||
* @param int $user_id ID of the user.
|
||||
* @param bool $reload Forces reading the role from the database.
|
||||
* @return string Role of the user.
|
||||
*/
|
||||
public static function get_role( $user_id ) {
|
||||
if ( isset( self::$user_roles[ $user_id ] ) ) {
|
||||
public static function get_role( $user_id, $reload = false ) {
|
||||
if ( ! $reload && isset( self::$user_roles[ $user_id ] ) ) {
|
||||
return self::$user_roles[ $user_id ];
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for attachments.
|
||||
*/
|
||||
|
||||
@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Defaults;
|
||||
use Automattic\Jetpack\Sync\Functions;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for callables.
|
||||
*/
|
||||
@ -291,17 +295,23 @@ class Callables extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $status This Module Full Sync Status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This Module Full Sync Status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $status ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_callables', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_callables', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -310,7 +320,7 @@ class Callables extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return 1;
|
||||
@ -384,6 +394,9 @@ class Callables extends Module {
|
||||
return;
|
||||
}
|
||||
$plugins = Functions::get_plugins();
|
||||
if ( ! is_array( $plugins ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||
/**
|
||||
* Plugins often like to unset things but things break if they are not able to.
|
||||
|
||||
@ -7,9 +7,14 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Sync\Defaults;
|
||||
use Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for comments.
|
||||
*/
|
||||
@ -201,21 +206,22 @@ class Comments extends Module {
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return array Defaults to [ '', 'trackback', 'pingback' ].
|
||||
* @return array Defaults to [ '', 'comment', 'trackback', 'pingback', 'review', 'note' ].
|
||||
*/
|
||||
public function get_whitelisted_comment_types() {
|
||||
/**
|
||||
* Comment types present in this list will sync their status changes to WordPress.com.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 7.6.0
|
||||
*
|
||||
* @param array A list of comment types.
|
||||
*/
|
||||
return apply_filters(
|
||||
'jetpack_sync_whitelisted_comment_types',
|
||||
array( '', 'comment', 'trackback', 'pingback', 'review' )
|
||||
);
|
||||
return Defaults::get_comment_types_whitelist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns escaped SQL for whitelisted comment types.
|
||||
* Can be injected directly into a WHERE clause.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return string SQL WHERE clause.
|
||||
*/
|
||||
public function get_whitelisted_comment_types_sql() {
|
||||
return 'comment_type IN (\'' . implode( '\', \'', array_map( 'esc_sql', $this->get_whitelisted_comment_types() ) ) . '\')';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -378,11 +384,13 @@ class Comments extends Module {
|
||||
* @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
|
||||
*/
|
||||
public function get_where_sql( $config ) {
|
||||
if ( is_array( $config ) ) {
|
||||
$where_sql = $this->get_whitelisted_comment_types_sql();
|
||||
|
||||
if ( is_array( $config ) && ! empty( $config ) ) {
|
||||
return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
|
||||
}
|
||||
|
||||
return '1=1';
|
||||
return $where_sql;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -490,6 +498,9 @@ class Comments extends Module {
|
||||
* @return array|boolean False if not whitelisted, the original hook args otherwise.
|
||||
*/
|
||||
public function filter_meta( $args ) {
|
||||
if ( ! is_array( $args ) || count( $args ) < 3 ) {
|
||||
return false;
|
||||
}
|
||||
if ( $this->is_comment_type_allowed( $args[1] ) && $this->is_whitelisted_comment_meta( $args[2] ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Sync\Defaults;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for constants.
|
||||
*/
|
||||
@ -146,17 +150,23 @@ class Constants extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $state This module Full Sync status.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This module Full Sync status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_constants', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_constants', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,7 +176,7 @@ class Constants extends Module {
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
*
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return 1;
|
||||
|
||||
@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Lock;
|
||||
use Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* This class does a full resync of the database by
|
||||
* sending an outbound action for every single object
|
||||
@ -65,21 +69,20 @@ class Full_Sync_Immediately extends Module {
|
||||
* @return bool Always returns true at success.
|
||||
*/
|
||||
public function start( $full_sync_config = null, $context = null ) {
|
||||
// There was a full sync in progress.
|
||||
if ( $this->is_started() && ! $this->is_finished() ) {
|
||||
/**
|
||||
* Fires when a full sync is cancelled.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
*/
|
||||
do_action( 'jetpack_full_sync_cancelled' );
|
||||
$this->send_action( 'jetpack_full_sync_cancelled' );
|
||||
}
|
||||
|
||||
// Check if there was a full sync in progress already before resetting the data.
|
||||
$should_process_cancelled_action = $this->get_status()['start_action_processed'] && ! $this->is_finished() ? true : false;
|
||||
// Remove all evidence of previous full sync items and status.
|
||||
$this->reset_data();
|
||||
|
||||
// Update status to indicate that a new full sync is starting and need to cancel previous one.
|
||||
if ( $should_process_cancelled_action ) {
|
||||
$this->update_status(
|
||||
array(
|
||||
'cancelled_action_processed' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! is_array( $full_sync_config ) ) {
|
||||
/*
|
||||
* Filter default sync config to allow injecting custom configuration.
|
||||
@ -102,29 +105,12 @@ class Full_Sync_Immediately extends Module {
|
||||
|
||||
$this->update_status(
|
||||
array(
|
||||
'started' => time(),
|
||||
'config' => $full_sync_config,
|
||||
'progress' => $this->get_initial_progress( $full_sync_config ),
|
||||
'started' => time(),
|
||||
'config' => $full_sync_config,
|
||||
'context' => $context,
|
||||
)
|
||||
);
|
||||
|
||||
$range = $this->get_content_range();
|
||||
/**
|
||||
* Fires when a full sync begins. This action is serialized
|
||||
* and sent to the server so that it knows a full sync is coming.
|
||||
*
|
||||
* @param array $full_sync_config Sync configuration for all sync modules.
|
||||
* @param array $range Range of the sync items, containing min and max IDs for some item types.
|
||||
* @param mixed $context The context where the full sync was initiated from.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
* @since-jetpack 7.3.0 Added $range arg.
|
||||
* @since 4.4.0 Added $context arg.
|
||||
*/
|
||||
do_action( 'jetpack_full_sync_start', $full_sync_config, $range );
|
||||
$this->send_action( 'jetpack_full_sync_start', array( $full_sync_config, $range, $context ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -148,10 +134,13 @@ class Full_Sync_Immediately extends Module {
|
||||
*/
|
||||
public function get_status() {
|
||||
$default = array(
|
||||
'started' => false,
|
||||
'finished' => false,
|
||||
'progress' => array(),
|
||||
'config' => array(),
|
||||
'start_action_processed' => false,
|
||||
'cancelled_action_processed' => true, // true by default to avoid sending the action when there is no need,
|
||||
'started' => false,
|
||||
'finished' => false,
|
||||
'progress' => array(),
|
||||
'config' => array(),
|
||||
'context' => null,
|
||||
);
|
||||
|
||||
return wp_parse_args( \Jetpack_Options::get_raw_option( self::STATUS_OPTION ), $default );
|
||||
@ -249,10 +238,11 @@ class Full_Sync_Immediately extends Module {
|
||||
* Given an initial Full Sync configuration get the initial status.
|
||||
*
|
||||
* @param array $full_sync_config Full sync configuration.
|
||||
* @param array $range Range of the sync items, containing min, max and count IDs for some item types.
|
||||
*
|
||||
* @return array Initial Sent status.
|
||||
*/
|
||||
public function get_initial_progress( $full_sync_config ) {
|
||||
public function get_initial_progress( $full_sync_config, $range = null ) {
|
||||
// Set default configuration, calculate totals, and save configuration if totals > 0.
|
||||
$status = array();
|
||||
foreach ( $full_sync_config as $name => $config ) {
|
||||
@ -261,7 +251,8 @@ class Full_Sync_Immediately extends Module {
|
||||
continue;
|
||||
}
|
||||
$status[ $name ] = array(
|
||||
'total' => $module->total( $config ),
|
||||
// If we have a range for the module, use the count from the range to avoid querying the database again.
|
||||
'total' => $range[ $name ]->count ?? $module->total( $config ),
|
||||
'sent' => 0,
|
||||
'finished' => false,
|
||||
);
|
||||
@ -275,18 +266,24 @@ class Full_Sync_Immediately extends Module {
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @param array $full_sync_config Full sync configuration.
|
||||
*
|
||||
* @return array Array of range (min ID, max ID, total items) for all content types.
|
||||
*/
|
||||
private function get_content_range() {
|
||||
$range = array();
|
||||
$config = $this->get_status()['config'];
|
||||
// Add range only when syncing all objects.
|
||||
if ( true === isset( $config['posts'] ) && $config['posts'] ) {
|
||||
$range['posts'] = $this->get_range( 'posts' );
|
||||
}
|
||||
|
||||
if ( true === isset( $config['comments'] ) && $config['comments'] ) {
|
||||
$range['comments'] = $this->get_range( 'comments' );
|
||||
private function get_content_range( $full_sync_config ) {
|
||||
$range = array();
|
||||
foreach ( $full_sync_config as $module_name => $config ) {
|
||||
// Calculate ranges only for modules that get chunked.
|
||||
if ( in_array( $module_name, array( 'constants', 'functions', 'network_options', 'options', 'themes', 'updates' ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
$module = Modules::get_module( $module_name );
|
||||
if ( ! $module ) {
|
||||
continue;
|
||||
}
|
||||
if ( true === isset( $config ) && $config ) {
|
||||
$range[ $module_name ] = $this->get_range( $module_name );
|
||||
}
|
||||
}
|
||||
|
||||
return $range;
|
||||
@ -303,23 +300,17 @@ class Full_Sync_Immediately extends Module {
|
||||
*/
|
||||
public function get_range( $type ) {
|
||||
global $wpdb;
|
||||
if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
|
||||
$module = Modules::get_module( $type );
|
||||
if ( ! $module ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'posts':
|
||||
$table = $wpdb->posts;
|
||||
$id = 'ID';
|
||||
$where_sql = Settings::get_blacklisted_post_types_sql();
|
||||
|
||||
break;
|
||||
case 'comments':
|
||||
$table = $wpdb->comments;
|
||||
$id = 'comment_ID';
|
||||
$where_sql = Settings::get_comments_filter_sql();
|
||||
break;
|
||||
$table = $module->table();
|
||||
$id = $module->id_field();
|
||||
if ( 'terms' === $module ) { // Terms module relies on the term_taxonomy and term_taxonomy_id for the where sql, let's use term_id instead.
|
||||
$id = 'term_id';
|
||||
}
|
||||
$where_sql = $module->get_where_sql( array() );
|
||||
|
||||
// TODO: Call $wpdb->prepare on the following query.
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
@ -385,6 +376,14 @@ class Full_Sync_Immediately extends Module {
|
||||
* @access public
|
||||
*/
|
||||
public function send() {
|
||||
|
||||
if ( ! $this->maybe_send_cancelled_action() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->maybe_send_full_sync_start() ) {
|
||||
return false;
|
||||
}
|
||||
$config = $this->get_status()['config'];
|
||||
|
||||
$max_duration = Settings::get_setting( 'full_sync_send_duration' );
|
||||
@ -394,22 +393,38 @@ class Full_Sync_Immediately extends Module {
|
||||
|
||||
$started = $this->get_status()['started'];
|
||||
|
||||
foreach ( $this->get_remaining_modules_to_send() as $module ) {
|
||||
$progress[ $module->name() ] = $module->send_full_sync_actions( $config[ $module->name() ], $progress[ $module->name() ], $send_until );
|
||||
if ( isset( $progress[ $module->name() ]['error'] ) ) {
|
||||
unset( $progress[ $module->name() ]['error'] );
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return false;
|
||||
} elseif ( ! $progress[ $module->name() ]['finished'] ) {
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return true;
|
||||
$remaining_modules = $this->get_remaining_modules_to_send();
|
||||
|
||||
foreach ( $remaining_modules as $module ) {
|
||||
$module_name = $module->name();
|
||||
if ( array_key_exists( $module_name, $progress ) && array_key_exists( $module_name, $config ) ) {
|
||||
$progress[ $module_name ] = $module->send_full_sync_actions( $config[ $module_name ], $progress[ $module_name ], $send_until, $started );
|
||||
if ( isset( $progress[ $module_name ]['error'] ) ) {
|
||||
unset( $progress[ $module_name ]['error'] );
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return false;
|
||||
} elseif ( ! $progress[ $module_name ]['finished'] ) {
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->get_status()['started'] !== $started ) {
|
||||
// Full sync was restarted, stop sending.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all remaining modules in progress are actually finished.
|
||||
// If a module was skipped in the main loop (due to being unfinished), but still exists in progress, we shouldn't mark the sync as complete.
|
||||
foreach ( $remaining_modules as $module ) {
|
||||
$name = $module->name();
|
||||
if ( array_key_exists( $name, $progress ) && empty( $progress[ $name ]['finished'] ) ) {
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->send_full_sync_end();
|
||||
$this->update_status( array( 'progress' => $progress ) );
|
||||
return true;
|
||||
@ -443,13 +458,101 @@ class Full_Sync_Immediately extends Module {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send 'jetpack_full_sync_end' and update 'finished' status.
|
||||
* Sends the `jetpack_full_sync_start` action if it hasn't been processed yet.
|
||||
*
|
||||
* Prepares the full sync start action, sends it to WordPress.com, fires the local action,
|
||||
* and updates the sync status to reflect that the start action has been processed.
|
||||
*
|
||||
* @return bool True if the action was successfully sent or already processed, false on failure.
|
||||
*/
|
||||
private function maybe_send_full_sync_start() {
|
||||
$status = $this->get_status();
|
||||
|
||||
// If already processed, nothing to do.
|
||||
if ( true === $status['start_action_processed'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$config = $status['config'];
|
||||
$context = $status['context'];
|
||||
$range = $this->get_content_range( $config );
|
||||
|
||||
$result = $this->send_action( 'jetpack_full_sync_start', array( $config, $range, $context ) );
|
||||
|
||||
// If the action failed on WordPress.com, return false.
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a full sync begins. This action is serialized
|
||||
* and sent to the server so that it knows a full sync is coming.
|
||||
*
|
||||
* @param array $config Sync configuration for all sync modules.
|
||||
* @param array $range Range of the sync items, containing min and max IDs for some item types.
|
||||
* @param mixed $context The context where the full sync was initiated from.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
* @since-jetpack 7.3.0 Added $range arg.
|
||||
* @since 4.4.0 Added $context arg.
|
||||
*/
|
||||
do_action( 'jetpack_full_sync_start', $config, $range );
|
||||
|
||||
$this->update_status(
|
||||
array(
|
||||
'start_action_processed' => true,
|
||||
'progress' => $this->get_initial_progress( $config, $range ),
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the `jetpack_full_sync_cancelled` action if it hasn't been processed yet.
|
||||
*
|
||||
* @return bool True if the action was successfully sent or already processed, false on failure.
|
||||
*/
|
||||
private function maybe_send_cancelled_action() {
|
||||
$status = $this->get_status();
|
||||
|
||||
if ( true === $status['cancelled_action_processed'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = $this->send_action( 'jetpack_full_sync_cancelled' );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a full sync is cancelled.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 4.2.0
|
||||
*/
|
||||
do_action( 'jetpack_full_sync_cancelled' );
|
||||
$this->update_status( array( 'cancelled_action_processed' => true ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the `jetpack_full_sync_end` action and updates the status when the full sync end action is processed.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function send_full_sync_end() {
|
||||
$range = $this->get_content_range();
|
||||
$status = $this->get_status();
|
||||
$range = $this->get_content_range( $status['config'] );
|
||||
$context = $status['context'];
|
||||
|
||||
$result = $this->send_action( 'jetpack_full_sync_end', array( '', $range, $context ) );
|
||||
|
||||
if ( is_wp_error( $result ) ) { // Do not set finished status if we get an error.
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Fires when a full sync ends. This action is serialized
|
||||
* and sent to the server.
|
||||
@ -462,7 +565,6 @@ class Full_Sync_Immediately extends Module {
|
||||
* @since-jetpack 7.3.0 Added $range arg.
|
||||
*/
|
||||
do_action( 'jetpack_full_sync_end', '', $range );
|
||||
$this->send_action( 'jetpack_full_sync_end', array( '', $range ) );
|
||||
|
||||
// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
|
||||
$this->update_status( array( 'finished' => time() ) );
|
||||
|
||||
@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Queue;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* This class does a full resync of the database by
|
||||
* enqueuing an outbound action for every single object
|
||||
@ -336,6 +340,10 @@ class Full_Sync extends Module {
|
||||
$id = 'comment_ID';
|
||||
$where_sql = Settings::get_comments_filter_sql();
|
||||
break;
|
||||
default:
|
||||
// This should never be reached due to the guard condition above,
|
||||
// but Phan complains so let's make it happy.
|
||||
return array();
|
||||
}
|
||||
|
||||
// TODO: Call $wpdb->prepare on the following query.
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for imports.
|
||||
*/
|
||||
@ -179,7 +183,7 @@ class Import extends Module {
|
||||
}
|
||||
|
||||
$action = current_filter();
|
||||
$backtrace = debug_backtrace( false ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
|
||||
$backtrace = debug_backtrace( 0 ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
|
||||
|
||||
$do_action_pos = -1;
|
||||
$backtrace_len = count( $backtrace );
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for menus.
|
||||
*/
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for meta.
|
||||
*/
|
||||
|
||||
@ -14,6 +14,10 @@ use Automattic\Jetpack\Sync\Replicastore;
|
||||
use Automattic\Jetpack\Sync\Sender;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic methods implemented by Jetpack Sync extensions.
|
||||
*
|
||||
@ -195,7 +199,7 @@ abstract class Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) {
|
||||
// In subclasses, return the number of items yet to be enqueued.
|
||||
@ -242,7 +246,12 @@ abstract class Module {
|
||||
if ( $sort && is_array( $values ) ) {
|
||||
$this->recursive_ksort( $values );
|
||||
}
|
||||
return crc32( wp_json_encode( Functions::json_wrap( $values ) ) );
|
||||
return crc32(
|
||||
wp_json_encode(
|
||||
Functions::json_wrap( $values ),
|
||||
0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because we don't want disrupt the checksum algorithm.
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -398,13 +407,14 @@ abstract class Module {
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @param string $config Full sync configuration for this module.
|
||||
* @param array $status the current module full sync status.
|
||||
* @param float $send_until timestamp until we want this request to send full sync events.
|
||||
* @param array $config Full sync configuration for this module.
|
||||
* @param array $status the current module full sync status.
|
||||
* @param float $send_until timestamp until we want this request to send full sync events.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array Status, the module full sync status updated.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $status, $send_until ) {
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( empty( $status['last_sent'] ) ) {
|
||||
@ -414,13 +424,25 @@ abstract class Module {
|
||||
$limits = Settings::get_setting( 'full_sync_limits' )[ $this->name() ] ??
|
||||
Defaults::get_default_setting( 'full_sync_limits' )[ $this->name() ] ??
|
||||
array(
|
||||
'max_chunks' => 10,
|
||||
'chunk_size' => 100,
|
||||
'max_chunks' => null,
|
||||
'chunk_size' => null,
|
||||
);
|
||||
|
||||
$limits = array(
|
||||
'max_chunks' => is_numeric( $limits['max_chunks'] ) ? (int) $limits['max_chunks'] : 10,
|
||||
'chunk_size' => is_numeric( $limits['chunk_size'] ) ? (int) $limits['chunk_size'] : 100,
|
||||
);
|
||||
|
||||
$limits['chunk_size'] = $this->adjust_chunk_size_if_stuck( $status['last_sent'], $limits['chunk_size'], $started );
|
||||
|
||||
$chunks_sent = 0;
|
||||
|
||||
$last_item = $this->get_last_item( $config );
|
||||
// Store last_item in status to avoid re-running this expensive query on every invocation.
|
||||
// The minimum ID does not change during a Full Sync.
|
||||
if ( ! isset( $status['last_item'] ) ) {
|
||||
$status['last_item'] = $this->get_last_item( $config );
|
||||
}
|
||||
$last_item = $status['last_item'];
|
||||
|
||||
while ( $chunks_sent < $limits['max_chunks'] && microtime( true ) < $send_until ) {
|
||||
$objects = $this->get_next_chunk( $config, $status, $limits['chunk_size'] );
|
||||
@ -437,7 +459,7 @@ abstract class Module {
|
||||
// If we have objects as a key it means get_next_chunk is being overridden, we need to check for it being an empty array.
|
||||
// In case it is an empty array, we should not send the action or increase the chunks_sent, we just need to update the status.
|
||||
if ( ! isset( $objects['objects'] ) || array() !== $objects['objects'] ) {
|
||||
$key = $this->full_sync_action_name() . '_' . crc32( wp_json_encode( $status['last_sent'] ) );
|
||||
$key = $this->full_sync_action_name() . '_' . crc32( wp_json_encode( $status['last_sent'], JSON_UNESCAPED_SLASHES ) );
|
||||
$result = $this->send_action( $this->full_sync_action_name(), array( $objects, $status['last_sent'] ), $key );
|
||||
if ( is_wp_error( $result ) || $wpdb->last_error ) {
|
||||
$status['error'] = true;
|
||||
@ -457,6 +479,72 @@ abstract class Module {
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust chunk size using adaptive logic and update transient for tracking stuck state.
|
||||
*
|
||||
* @param string $last_sent The current last_sent marker.
|
||||
* @param int $default_chunk_size The default chunk size.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
* @return int Adjusted chunk size.
|
||||
*/
|
||||
private function adjust_chunk_size_if_stuck( $last_sent, $default_chunk_size, $started ) {
|
||||
$transient_key = 'jetpack_sync_last_sent_' . $this->name() . '_' . $started;
|
||||
$stuck_data = get_transient( $transient_key );
|
||||
$is_stuck = isset( $stuck_data['last_sent'] ) && $stuck_data['last_sent'] === $last_sent;
|
||||
|
||||
// Preserve the adjusted chunk size and stuck count from the transient when stuck.
|
||||
$stuck_count = $is_stuck && isset( $stuck_data['stuck_count'] ) ? $stuck_data['stuck_count'] : 0;
|
||||
$adjusted_chunk_size = $is_stuck && isset( $stuck_data['adjusted_chunk_size'] ) ? $stuck_data['adjusted_chunk_size'] : $default_chunk_size;
|
||||
|
||||
if ( $is_stuck && $stuck_data['adjusted_chunk_size'] === 1 ) {
|
||||
// Refresh transient TTL to prevent expiry-driven reset cycles.
|
||||
set_transient( $transient_key, $stuck_data, HOUR_IN_SECONDS );
|
||||
return 1; // If we are already at the minimum chunk size, do not adjust further.
|
||||
}
|
||||
|
||||
// We will adjust if it is stuck after 10 minutes.
|
||||
if (
|
||||
$is_stuck &&
|
||||
( time() - $stuck_data['timestamp'] ) >= 10 * MINUTE_IN_SECONDS
|
||||
) {
|
||||
++$stuck_count;
|
||||
$adjusted_chunk_size = max( 1, (int) ( $default_chunk_size / ( 2 ** $stuck_count ) ) ); // Halve the chunk size for each stuck iteration.
|
||||
|
||||
// Send one HTTP notification when chunk size reaches the minimum (1)
|
||||
// so the stuck state is visible for monitoring. Intermediate cascade
|
||||
// steps skip the HTTP request to avoid consuming the time budget.
|
||||
if ( 1 === $adjusted_chunk_size ) {
|
||||
$this->send_action(
|
||||
'jetpack_full_sync_stuck_adjustment',
|
||||
array(
|
||||
'module' => $this->name(),
|
||||
'last_sent' => $last_sent,
|
||||
'stuck_count' => $stuck_count,
|
||||
'adjusted_chunk_size' => $adjusted_chunk_size,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set or update the transient with the new last_sent, timestamp, and stuck_count.
|
||||
// Reset the timestamp when not stuck or after an adjustment, so each new chunk size
|
||||
// gets a 10-minute window to prove itself before halving further.
|
||||
$previous_chunk_size = $stuck_data['adjusted_chunk_size'] ?? null;
|
||||
$reset_timestamp = ! $is_stuck || $adjusted_chunk_size !== $previous_chunk_size;
|
||||
set_transient(
|
||||
$transient_key,
|
||||
array(
|
||||
'last_sent' => $last_sent,
|
||||
'timestamp' => $reset_timestamp ? time() : $stuck_data['timestamp'],
|
||||
'stuck_count' => $stuck_count,
|
||||
'adjusted_chunk_size' => $adjusted_chunk_size,
|
||||
),
|
||||
HOUR_IN_SECONDS
|
||||
);
|
||||
|
||||
return $adjusted_chunk_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status of the full sync action based on the objects that were sent.
|
||||
* Used to update the status of the module after sending a chunk of objects.
|
||||
@ -737,7 +825,7 @@ abstract class Module {
|
||||
$current_size = 0;
|
||||
|
||||
foreach ( $objects as $object ) {
|
||||
$object_size = strlen( maybe_serialize( $object ) );
|
||||
$object_size = strlen( (string) maybe_serialize( $object ) );
|
||||
$current_metadata = array();
|
||||
$metadata_size = 0;
|
||||
$id_field = $this->id_field();
|
||||
@ -745,12 +833,13 @@ abstract class Module {
|
||||
|
||||
foreach ( $metadata as $key => $metadata_item ) {
|
||||
if ( (int) $metadata_item->{$type . '_id'} === $object_id ) {
|
||||
$metadata_item_size = strlen( maybe_serialize( $metadata_item->meta_value ) );
|
||||
$metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
|
||||
if ( $metadata_item_size >= $max_meta_size ) {
|
||||
$metadata_item->meta_value = ''; // Trim metadata if too large.
|
||||
$metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
|
||||
}
|
||||
$current_metadata[] = $metadata_item;
|
||||
$metadata_size += $metadata_item_size >= $max_meta_size ? 0 : $metadata_item_size;
|
||||
$metadata_size += $metadata_item_size;
|
||||
|
||||
if ( ! empty( $filtered_object_ids ) && ( $current_size + $object_size + $metadata_size ) > $max_total_size ) {
|
||||
break 2; // Exit both loops.
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Sync\Defaults;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for network options.
|
||||
*/
|
||||
@ -120,17 +124,23 @@ class Network_Options extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $state This module Full Sync status.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This module Full Sync status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_network_options', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_network_options', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,7 +149,7 @@ class Network_Options extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return 1;
|
||||
|
||||
@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Defaults;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for options.
|
||||
*/
|
||||
@ -172,17 +176,23 @@ class Options extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $state This module Full Sync status.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This module Full Sync status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_options', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_options', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,6 +317,15 @@ class Options extends Module {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if 'jetpack_options' were updated and reject if the change affected only blacklisted keys.
|
||||
if ( 'jetpack_options' === $args[0] && 3 === count( $args ) ) {
|
||||
if ( ! $this->should_enqueue_jetpack_options_update( $args[1], $args[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
// Filter our weird array( false ) value for theme_mods_*.
|
||||
if ( str_starts_with( $args[0], 'theme_mods_' ) ) {
|
||||
$this->filter_theme_mods( $args[1] );
|
||||
@ -477,4 +496,49 @@ class Options extends Module {
|
||||
|
||||
return 'OPTION-DOES-NOT-EXIST';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if 'jetpack_options' option update should be processed based on excluded keys.
|
||||
*
|
||||
* @param mixed $old_value The old option value.
|
||||
* @param mixed $value The new option value.
|
||||
* @return bool False if only excluded keys changed (or no change), true otherwise.
|
||||
*/
|
||||
private function should_enqueue_jetpack_options_update( $old_value, $value ) {
|
||||
// No changes at all.
|
||||
if ( $old_value === $value ) {
|
||||
return false;
|
||||
}
|
||||
// Values are different but not both arrays - meaningful change.
|
||||
if ( ! is_array( $old_value ) || ! is_array( $value ) ) {
|
||||
return true;
|
||||
}
|
||||
// Determine all top-level keys present in either the old or new value.
|
||||
$all_keys = array_unique(
|
||||
array_merge(
|
||||
array_keys( $old_value ),
|
||||
array_keys( $value )
|
||||
)
|
||||
);
|
||||
// Short-circuit as soon as we find a changed key that is not blacklisted.
|
||||
foreach ( $all_keys as $key ) {
|
||||
$old_has_key = array_key_exists( $key, $old_value );
|
||||
$new_has_key = array_key_exists( $key, $value );
|
||||
// Key was added or removed.
|
||||
if ( ! $old_has_key || ! $new_has_key ) {
|
||||
if ( ! in_array( $key, Defaults::$jetpack_options_blacklist, true ) ) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Key exists in both arrays but the value changed.
|
||||
if ( $old_value[ $key ] !== $value[ $key ] ) {
|
||||
if ( ! in_array( $key, Defaults::$jetpack_options_blacklist, true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Either there were no effective changes, or all changed keys are excluded.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Constants as Jetpack_Constants;
|
||||
use WP_Error;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for plugins.
|
||||
*/
|
||||
@ -50,6 +54,24 @@ class Plugins extends Module {
|
||||
*/
|
||||
private $plugins_updated = array();
|
||||
|
||||
/**
|
||||
* List of plugins installed during this request.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $plugins_installed = array();
|
||||
|
||||
/**
|
||||
* List of all plugin update failures during this request.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $plugins_update_failures = array();
|
||||
|
||||
/**
|
||||
* State
|
||||
*
|
||||
@ -154,20 +176,16 @@ class Plugins extends Module {
|
||||
);
|
||||
$errors = $this->get_errors( $upgrader->skin );
|
||||
if ( $errors ) {
|
||||
foreach ( $plugins as $slug ) {
|
||||
/**
|
||||
* Sync that a plugin update failed
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 5.8.0
|
||||
*
|
||||
* @module sync
|
||||
*
|
||||
* @param string $plugin , Plugin slug
|
||||
* @param string Error code
|
||||
* @param string Error message
|
||||
*/
|
||||
do_action( 'jetpack_plugin_update_failed', $this->get_plugin_info( $slug ), $errors['code'], $errors['message'], $this->state );
|
||||
foreach ( $plugins as $slug ) { // Accumulate failures and defer to shutdown, to reduce request-time lag.
|
||||
$this->plugins_update_failures[] = array(
|
||||
'plugin' => $this->get_plugin_info( $slug ),
|
||||
'code' => $errors['code'],
|
||||
'message' => $errors['message'],
|
||||
'state' => $this->state,
|
||||
);
|
||||
}
|
||||
if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ) ) ) {
|
||||
add_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ), 9 );
|
||||
}
|
||||
|
||||
return;
|
||||
@ -178,18 +196,16 @@ class Plugins extends Module {
|
||||
|
||||
break;
|
||||
case 'install':
|
||||
/**
|
||||
* Signals to the sync listener that a plugin was installed and a sync action
|
||||
* reflecting the installation and the plugin info should be sent
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 5.8.0
|
||||
*
|
||||
* @module sync
|
||||
*
|
||||
* @param array () $plugin, Plugin Data
|
||||
*/
|
||||
do_action( 'jetpack_plugin_installed', array_map( array( $this, 'get_plugin_info' ), $plugins ) );
|
||||
// Accumulate installs and defer to shutdown.
|
||||
$this->plugins_installed = array_merge(
|
||||
$this->plugins_installed,
|
||||
array_map( array( $this, 'get_plugin_info' ), $plugins )
|
||||
);
|
||||
if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_installed' ) ) ) {
|
||||
add_action( 'shutdown', array( $this, 'sync_plugins_installed' ), 9 );
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,4 +420,54 @@ class Plugins extends Module {
|
||||
*/
|
||||
do_action( 'jetpack_plugins_updated', $this->plugins_updated, $this->state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for firing the 'jetpack_plugin_installed' action on shutdown.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function sync_plugins_installed() {
|
||||
if ( empty( $this->plugins_installed ) ) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Signals to the sync listener that a plugin was installed and a sync action
|
||||
* reflecting the installation and the plugin info should be sent.
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 5.8.0
|
||||
*
|
||||
* @module sync
|
||||
*
|
||||
* @param array () $plugin, Plugin Data
|
||||
*/
|
||||
do_action( 'jetpack_plugin_installed', $this->plugins_installed );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for firing the 'jetpack_plugin_update_failed' actions on shutdown.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function sync_plugins_update_failed() {
|
||||
if ( empty( $this->plugins_update_failures ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $this->plugins_update_failures as $failure ) {
|
||||
/**
|
||||
* Sync that a plugin update failed
|
||||
*
|
||||
* @since 1.6.3
|
||||
* @since-jetpack 5.8.0
|
||||
*
|
||||
* @module sync
|
||||
*
|
||||
* @param array $plugin Plugin Data
|
||||
* @param string $code Error code
|
||||
* @param string $message Error message
|
||||
* @param array $state State data
|
||||
*/
|
||||
do_action( 'jetpack_plugin_update_failed', $failure['plugin'], $failure['code'], $failure['message'], $failure['state'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,10 @@ use Automattic\Jetpack\Roles;
|
||||
use Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for posts.
|
||||
*/
|
||||
@ -43,6 +47,15 @@ class Posts extends Module {
|
||||
*/
|
||||
private $action_handler;
|
||||
|
||||
/**
|
||||
* Mark posts that are deleted in the current request.
|
||||
*
|
||||
* @access private
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $deleted_posts_in_request = array();
|
||||
|
||||
/**
|
||||
* Import end.
|
||||
*
|
||||
@ -140,6 +153,7 @@ class Posts extends Module {
|
||||
public function init_listeners( $callable ) {
|
||||
$this->action_handler = $callable;
|
||||
|
||||
add_action( 'before_delete_post', array( $this, 'mark_post_is_being_deleted' ), 0, 1 );
|
||||
add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ), 11, 3 );
|
||||
add_action( 'wp_after_insert_post', array( $this, 'wp_after_insert_post' ), 11, 2 );
|
||||
add_action( 'jetpack_sync_save_post', $callable, 10, 4 );
|
||||
@ -154,12 +168,17 @@ class Posts extends Module {
|
||||
$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_deleted_post_meta', array( $this, 'maybe_skip_deleted_post_meta' ) );
|
||||
|
||||
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' ) );
|
||||
|
||||
add_action( 'jetpack_daily_akismet_meta_cleanup_before', array( $this, 'daily_akismet_meta_cleanup_before' ) );
|
||||
add_action( 'jetpack_daily_akismet_meta_cleanup_after', array( $this, 'daily_akismet_meta_cleanup_after' ) );
|
||||
add_action( 'jetpack_post_meta_batch_delete', $callable, 10, 2 );
|
||||
|
||||
add_action( 'deleted_post', array( $this, 'unmark_post_being_deleted' ), 11, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,8 +241,8 @@ class Posts extends Module {
|
||||
*/
|
||||
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_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.
|
||||
$sync_module = Modules::get_module( 'full-sync' );
|
||||
@ -258,7 +277,7 @@ class Posts extends Module {
|
||||
* @todo Use $wpdb->prepare for the SQL query.
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) {
|
||||
global $wpdb;
|
||||
@ -282,7 +301,7 @@ class Posts extends Module {
|
||||
$where_sql = Settings::get_blacklisted_post_types_sql();
|
||||
|
||||
// Config is a list of post IDs to sync.
|
||||
if ( is_array( $config ) ) {
|
||||
if ( is_array( $config ) && ! empty( $config ) ) {
|
||||
$where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
|
||||
}
|
||||
|
||||
@ -312,12 +331,116 @@ class Posts extends Module {
|
||||
// Explicitly truncate meta_value when it exceeds limit.
|
||||
// Large content will cause OOM issues and break Sync.
|
||||
$serialized_value = maybe_serialize( $meta_value );
|
||||
if ( strlen( $serialized_value ) >= self::MAX_META_LENGTH ) {
|
||||
if ( $serialized_value === null || strlen( $serialized_value ) >= self::MAX_META_LENGTH ) {
|
||||
$meta_value = '';
|
||||
}
|
||||
return array( $meta_id, $object_id, $meta_key, $meta_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated 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_updated_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 );
|
||||
if ( is_array( $current_value ) && ! empty( $current_value ) ) {
|
||||
$meta_value = $current_value;
|
||||
}
|
||||
return $this->trim_post_meta( array( $meta_id, $object_id, $meta_key, $meta_value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|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 ) {
|
||||
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 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a post as being deleted in the current request.
|
||||
*
|
||||
* @param int $post_id ID of the post being deleted.
|
||||
*/
|
||||
public function mark_post_is_being_deleted( $post_id ) {
|
||||
self::$deleted_posts_in_request[ (int) $post_id ] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue-time per-request dedupe for deleted post metadata, if the post itself is being deleted.
|
||||
*
|
||||
* @param array $args [ $meta_id, $post_id, $meta_key, $meta_value ].
|
||||
* @return array|false
|
||||
*/
|
||||
public function maybe_skip_deleted_post_meta( $args ) {
|
||||
if ( is_array( $args ) && isset( $args[1] ) && is_numeric( $args[1] ) ) {
|
||||
$post_id = (int) $args[1];
|
||||
if ( isset( self::$deleted_posts_in_request[ $post_id ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark a post as being deleted in the current request, to clean up.
|
||||
*
|
||||
* @param int $post_id ID of the post.
|
||||
*/
|
||||
public function unmark_post_being_deleted( $post_id ) {
|
||||
unset( self::$deleted_posts_in_request[ (int) $post_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 on_before_enqueue_updated_attachment_metadata( $args ) {
|
||||
if ( ! is_array( $args ) || count( $args ) < 3 ) {
|
||||
return $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;
|
||||
}
|
||||
$seen_updated_meta_for_post[ $post_id ] = true;
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process content before send.
|
||||
*
|
||||
@ -337,25 +460,47 @@ class Posts extends Module {
|
||||
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
|
||||
*/
|
||||
public function filter_jetpack_sync_before_enqueue_jetpack_sync_save_post( $args ) {
|
||||
list( $post_id, $post, $update, $previous_state ) = $args;
|
||||
if (
|
||||
! is_array( $args )
|
||||
|| ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] )
|
||||
|| ! array_key_exists( 1, $args ) || ! ( $args[1] instanceof \WP_Post )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list( $post_id, $post, $update, $previous_state ) = array_pad( $args, 4, null );
|
||||
|
||||
if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
|
||||
// During incremental sync, skip posts whose type is not registered (e.g. CPT unregistered before sync).
|
||||
// Full sync may have already sent them; we simply don't enqueue incremental updates for them.
|
||||
if ( ! get_post_type_object( $post->post_type ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array( (int) $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add filtered post content.
|
||||
*
|
||||
* @param array $args Hook arguments.
|
||||
* @return array Hook arguments.
|
||||
* @return array|false Hook arguments, or false if the arguments are invalid.
|
||||
*/
|
||||
public function filter_jetpack_sync_before_enqueue_jetpack_published_post( $args ) {
|
||||
list( $post_id, $flags, $post ) = $args;
|
||||
if (
|
||||
! is_array( $args )
|
||||
|| ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] )
|
||||
|| ! array_key_exists( 1, $args ) || ! is_array( $args[1] )
|
||||
|| ! array_key_exists( 2, $args ) || ! ( $args[2] instanceof \WP_Post )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array( $post_id, $flags, $this->filter_post_content_and_add_links( $post ) );
|
||||
list( $post_id, $flags, $post ) = $args;
|
||||
return array( (int) $post_id, $flags, $this->filter_post_content_and_add_links( $post ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,7 +510,9 @@ class Posts extends Module {
|
||||
* @return array|false Hook arguments, or false if the post type is a blacklisted one.
|
||||
*/
|
||||
public function filter_blacklisted_post_types_deleted( $args ) {
|
||||
|
||||
if ( ! is_array( $args ) || ! array_key_exists( 0, $args ) || ! is_numeric( $args[0] ) ) {
|
||||
return false;
|
||||
}
|
||||
// deleted_post is called after the SQL delete but before cache cleanup.
|
||||
// There is the potential we can't detect post_type at this point.
|
||||
if ( ! $this->is_post_type_allowed( $args[0] ) ) {
|
||||
@ -378,10 +525,16 @@ class Posts extends Module {
|
||||
/**
|
||||
* 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 ) || count( $args ) < 3 ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! is_numeric( $args[1] ) || ! is_string( $args[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) {
|
||||
return $args;
|
||||
}
|
||||
@ -396,8 +549,11 @@ class Posts extends Module {
|
||||
* @return boolean 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_' );
|
||||
if ( ! is_string( $meta_key ) ) {
|
||||
return false;
|
||||
}
|
||||
// The '_wpas_skip_' meta key prefix is used by Publicize to mark posts that should be skipped.
|
||||
return str_starts_with( $meta_key, '_wpas_skip_' ) || in_array( $meta_key, Settings::get_setting( 'post_meta_whitelist' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -597,6 +753,9 @@ class Posts extends Module {
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public function save_published( $new_status, $old_status, $post ) {
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return;
|
||||
}
|
||||
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
|
||||
$this->just_published[ $post->ID ] = true;
|
||||
}
|
||||
@ -634,12 +793,12 @@ class Posts extends Module {
|
||||
* @param boolean $update Whether this is an existing post being updated or not.
|
||||
*/
|
||||
public function wp_insert_post( $post_ID, $post = null, $update = null ) {
|
||||
if ( ! is_numeric( $post_ID ) || $post === null ) {
|
||||
if ( ! is_numeric( $post_ID ) || ! $post instanceof \WP_Post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
|
||||
if ( $post && 'shop_order' === $post->post_type ) {
|
||||
if ( 'shop_order' === $post->post_type ) {
|
||||
$post = get_post( $post_ID );
|
||||
}
|
||||
|
||||
@ -678,12 +837,12 @@ class Posts extends Module {
|
||||
* @param \WP_Post $post Post object.
|
||||
**/
|
||||
public function wp_after_insert_post( $post_ID, $post ) {
|
||||
if ( ! is_numeric( $post_ID ) || $post === null ) {
|
||||
if ( ! is_numeric( $post_ID ) || ! $post instanceof \WP_Post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/woocommerce/woocommerce/issues/18007.
|
||||
if ( $post && 'shop_order' === $post->post_type ) {
|
||||
if ( 'shop_order' === $post->post_type ) {
|
||||
$post = get_post( $post_ID );
|
||||
}
|
||||
|
||||
@ -756,6 +915,9 @@ class Posts extends Module {
|
||||
*/
|
||||
if ( 'customize_changeset' === $post->post_type ) {
|
||||
$post_content = json_decode( $post->post_content, true );
|
||||
if ( ! is_iterable( $post_content ) ) {
|
||||
return;
|
||||
}
|
||||
foreach ( $post_content as $key => $value ) {
|
||||
// Skip if it isn't a widget.
|
||||
if ( 'widget_' !== substr( $key, 0, strlen( 'widget_' ) ) ) {
|
||||
@ -771,7 +933,7 @@ class Posts extends Module {
|
||||
$widget_data = array(
|
||||
'name' => $wp_registered_widgets[ $key ]['name'],
|
||||
'id' => $key,
|
||||
'title' => $value['value']['title'],
|
||||
'title' => $value['value']['title'] ?? '',
|
||||
);
|
||||
do_action( 'jetpack_widget_edited', $widget_data );
|
||||
}
|
||||
|
||||
@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Constants as Jetpack_Constants;
|
||||
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for Protect.
|
||||
* Logs BruteProtect failed logins via sync.
|
||||
@ -50,7 +54,7 @@ class Protect extends Module {
|
||||
return $brute_force_protection->has_login_ability();
|
||||
}
|
||||
|
||||
// If the login ability can not be determined, the feature is not active,
|
||||
// If the login ability cannot be determined, the feature is not active,
|
||||
// or something is wrong, default to not syncing failed login attempts.
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for Jetpack Search.
|
||||
*/
|
||||
@ -437,6 +441,10 @@ class Search extends Module {
|
||||
'_wc_general_product_dependency_theme' => array(),
|
||||
'_wc_general_product_dependency_plugin' => array(),
|
||||
'wpcom_marketplace_product_extra_fields' => array(),
|
||||
'wccom_product_search_keywords' => array( 'searchable_in_all_content' => true ),
|
||||
'_wccom_product_faqs' => array( 'searchable_in_all_content' => true ),
|
||||
'wccom_product_features' => array( 'searchable_in_all_content' => true ),
|
||||
'wccom_product_compatibility' => array( 'searchable_in_all_content' => true ),
|
||||
|
||||
); // end indexed post meta.
|
||||
|
||||
@ -764,6 +772,7 @@ class Search extends Module {
|
||||
'translation_priority',
|
||||
|
||||
// woocommerce.
|
||||
'documentation_category',
|
||||
'pa_accessory-type',
|
||||
'pa_actor',
|
||||
'pa_age',
|
||||
@ -802,6 +811,7 @@ class Search extends Module {
|
||||
'pa_colour',
|
||||
'pa_compactor',
|
||||
'pa_condition',
|
||||
'pa_conditions-options',
|
||||
'pa_cor',
|
||||
'pa_couleur',
|
||||
'pa_country',
|
||||
@ -870,6 +880,7 @@ class Search extends Module {
|
||||
'pa_high-blow-tank',
|
||||
'pa_hoehe',
|
||||
'pa_inhoud',
|
||||
'pa_interchange-part-number',
|
||||
'pa_isadultproduct',
|
||||
'pa_isbn',
|
||||
'pa_iseligiblefortradein',
|
||||
@ -1243,6 +1254,7 @@ class Search extends Module {
|
||||
'group',
|
||||
'group-documents-category',
|
||||
'groups',
|
||||
'guest',
|
||||
'hashtags',
|
||||
'hotel_facility',
|
||||
'ia_invited_groups',
|
||||
@ -1321,6 +1333,7 @@ class Search extends Module {
|
||||
'organization',
|
||||
'our_team_category',
|
||||
'page_category',
|
||||
'page_condition',
|
||||
'parisrestaurant',
|
||||
'parissauna',
|
||||
'partner_category',
|
||||
@ -1733,6 +1746,9 @@ class Search extends Module {
|
||||
'kb_category',
|
||||
'kb_tag',
|
||||
|
||||
// coolhunting.com
|
||||
'article-type',
|
||||
|
||||
); // end taxonomies.
|
||||
|
||||
/**
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Heartbeat;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for stats.
|
||||
*/
|
||||
@ -29,30 +33,20 @@ class Stats extends Module {
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param callable $callback Action handler callable.
|
||||
* @param callable $callback Unused - required for parent class compatibility.
|
||||
*/
|
||||
public function init_listeners( $callback ) {
|
||||
public function init_listeners( $callback ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
add_action( 'jetpack_heartbeat', array( $this, 'sync_site_stats' ), 20 );
|
||||
add_action( 'jetpack_sync_heartbeat_stats', $callback );
|
||||
}
|
||||
|
||||
/**
|
||||
* This namespaces the action that we sync.
|
||||
* So that we can differentiate it from future actions.
|
||||
* Send Heartbeat stats immediately.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function sync_site_stats() {
|
||||
do_action( 'jetpack_sync_heartbeat_stats' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module in the sender.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function init_before_send() {
|
||||
add_filter( 'jetpack_sync_before_send_jetpack_sync_heartbeat_stats', array( $this, 'add_stats' ) );
|
||||
$this->send_action( 'jetpack_sync_heartbeat_stats', $this->add_stats() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
use Automattic\Jetpack\Sync\Listener;
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for term relationships.
|
||||
*/
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Sync\Settings;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for terms.
|
||||
*/
|
||||
@ -204,7 +208,7 @@ class Terms extends Module {
|
||||
public function get_where_sql( $config ) {
|
||||
$where_sql = Settings::get_blacklisted_taxonomies_sql();
|
||||
|
||||
if ( is_array( $config ) ) {
|
||||
if ( is_array( $config ) && ! empty( $config ) ) {
|
||||
$where_sql .= ' AND term_taxonomy_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
|
||||
}
|
||||
|
||||
@ -339,12 +343,14 @@ class Terms extends Module {
|
||||
list( $term_taxonomy_ids, $previous_end ) = $args;
|
||||
|
||||
return array(
|
||||
// @phan-suppress-next-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
|
||||
'terms' => get_terms(
|
||||
array(
|
||||
'hide_empty' => false,
|
||||
'term_taxonomy_id' => $term_taxonomy_ids,
|
||||
'orderby' => 'term_taxonomy_id',
|
||||
'order' => 'DESC',
|
||||
'taxonomy' => array(),
|
||||
'term_taxonomy_id' => $term_taxonomy_ids,
|
||||
)
|
||||
),
|
||||
'previous_end' => $previous_end,
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for themes.
|
||||
*/
|
||||
@ -509,17 +513,23 @@ class Themes extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $state This module Full Sync status.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This module Full Sync status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_theme_data', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_theme_data', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -528,7 +538,7 @@ class Themes extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return 1;
|
||||
@ -735,6 +745,7 @@ class Themes extends Module {
|
||||
|
||||
$moved_to_inactive_ids = array();
|
||||
$moved_to_sidebar = array();
|
||||
$new_inactive_widgets = $new_value['wp_inactive_widgets'] ?? array();
|
||||
|
||||
foreach ( $new_value as $sidebar => $new_widgets ) {
|
||||
if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ), true ) ) {
|
||||
@ -747,8 +758,7 @@ class Themes extends Module {
|
||||
if ( ! is_array( $new_widgets ) ) {
|
||||
$new_widgets = array();
|
||||
}
|
||||
|
||||
$moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_value['wp_inactive_widgets'] );
|
||||
$moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_inactive_widgets );
|
||||
$moved_to_inactive_ids = array_merge( $moved_to_inactive_ids, $moved_to_inactive_recently );
|
||||
|
||||
$moved_to_sidebar_recently = $this->sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar );
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Constants as Jetpack_Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for updates.
|
||||
*/
|
||||
@ -230,9 +234,10 @@ class Updates extends Module {
|
||||
switch ( $transient ) {
|
||||
case 'update_plugins':
|
||||
if ( ! empty( $update->response ) && is_array( $update->response ) ) {
|
||||
foreach ( $update->response as $plugin_slug => $response ) {
|
||||
if ( ! empty( $plugin_slug ) && isset( $response->new_version ) ) {
|
||||
$updates[] = array( $plugin_slug => $response->new_version );
|
||||
foreach ( $update->response as $plugin_slug => $plugin_data ) {
|
||||
$plugin_data = (array) $plugin_data;
|
||||
if ( ! empty( $plugin_slug ) && isset( $plugin_data['new_version'] ) ) {
|
||||
$updates[] = array( $plugin_slug => $plugin_data['new_version'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -247,9 +252,10 @@ class Updates extends Module {
|
||||
break;
|
||||
case 'update_themes':
|
||||
if ( ! empty( $update->response ) && is_array( $update->response ) ) {
|
||||
foreach ( $update->response as $theme_slug => $response ) {
|
||||
if ( ! empty( $theme_slug ) && isset( $response['new_version'] ) ) {
|
||||
$updates[] = array( $theme_slug => $response['new_version'] );
|
||||
foreach ( $update->response as $theme_slug => $theme_data ) {
|
||||
$theme_data = (array) $theme_data;
|
||||
if ( ! empty( $theme_slug ) && isset( $theme_data['new_version'] ) ) {
|
||||
$updates[] = array( $theme_slug => $theme_data['new_version'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,12 +267,12 @@ class Updates extends Module {
|
||||
break;
|
||||
case 'update_core':
|
||||
if ( ! empty( $update->updates ) && is_array( $update->updates ) ) {
|
||||
foreach ( $update->updates as $response ) {
|
||||
if ( ! empty( $response->response ) && 'latest' === $response->response ) {
|
||||
foreach ( $update->updates as $core_update ) {
|
||||
if ( ! empty( $core_update->response ) && 'latest' === $core_update->response ) {
|
||||
continue;
|
||||
}
|
||||
if ( ! empty( $response->response ) && isset( $response->packages->full ) ) {
|
||||
$updates[] = array( $response->response => $response->packages->full );
|
||||
if ( ! empty( $core_update->response ) && isset( $core_update->packages->full ) ) {
|
||||
$updates[] = array( $core_update->response => $core_update->packages->full );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -389,17 +395,23 @@ class Updates extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $send_until The timestamp until the current request can send.
|
||||
* @param array $state This module Full Sync status.
|
||||
* @param int $started The timestamp when the full sync started.
|
||||
*
|
||||
* @return array This module Full Sync status.
|
||||
*/
|
||||
public function send_full_sync_actions( $config, $send_until, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// we call this instead of do_action when sending immediately.
|
||||
$this->send_action( 'jetpack_full_sync_updates', array( true ) );
|
||||
$result = $this->send_action( 'jetpack_full_sync_updates', array( true ) );
|
||||
|
||||
// The number of actions enqueued, and next module state (true == done).
|
||||
return array( 'finished' => true );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$status['error'] = true;
|
||||
return $status;
|
||||
}
|
||||
$status['finished'] = true;
|
||||
$status['sent'] = $status['total'];
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -408,7 +420,7 @@ class Updates extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
return 1;
|
||||
@ -502,12 +514,16 @@ class Updates extends Module {
|
||||
}
|
||||
if ( ! is_array( $args[0]->response ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response ) . ' instead', E_USER_WARNING );
|
||||
trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response, JSON_UNESCAPED_SLASHES ) . ' instead', E_USER_WARNING );
|
||||
return $args;
|
||||
}
|
||||
foreach ( $args[0]->response as $stylesheet => &$theme_data ) {
|
||||
$theme = wp_get_theme( $stylesheet );
|
||||
$theme_data['name'] = $theme->name;
|
||||
$theme_data = (array) $theme_data;
|
||||
// Make sure the theme data array is not empty and has data that would indicate it is in the correct format.
|
||||
if ( isset( $theme_data['theme'] ) ) {
|
||||
$theme = wp_get_theme( $stylesheet );
|
||||
$theme_data['name'] = $theme->name;
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
@ -7,10 +7,15 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Constants as Jetpack_Constants;
|
||||
use Automattic\Jetpack\Password_Checker;
|
||||
use Automattic\Jetpack\Sync\Defaults;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for users.
|
||||
*/
|
||||
@ -137,7 +142,7 @@ class Users extends Module {
|
||||
|
||||
add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 );
|
||||
add_action( 'jetpack_deleted_user', $callable, 10, 3 );
|
||||
add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 );
|
||||
add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 3 );
|
||||
add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
|
||||
|
||||
// User roles.
|
||||
@ -249,6 +254,8 @@ class Users extends Module {
|
||||
$user->locale = get_user_locale( $user->ID );
|
||||
}
|
||||
|
||||
$user->is_connected = ( new Manager( 'jetpack' ) )->is_user_connected( $user->ID );
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@ -357,7 +364,11 @@ class Users extends Module {
|
||||
* @param string $user_login The user login.
|
||||
* @param \WP_User $user The user object.
|
||||
*/
|
||||
public function wp_login_handler( $user_login, $user ) {
|
||||
public function wp_login_handler( $user_login, $user = null ) {
|
||||
if ( ! $user instanceof \WP_User || empty( $user->ID ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a user is logged into a site.
|
||||
*
|
||||
@ -621,7 +632,9 @@ class Users extends Module {
|
||||
);
|
||||
|
||||
// The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both.
|
||||
if ( $this->is_create_user() || $this->is_add_user_to_blog() ) {
|
||||
if ( $this->is_function_in_backtrace(
|
||||
array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions() )
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
@ -692,7 +705,9 @@ class Users extends Module {
|
||||
$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
|
||||
}
|
||||
|
||||
if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
|
||||
if ( $this->is_function_in_backtrace(
|
||||
array_merge( $this->get_create_user_functions(), $this->get_add_user_to_blog_functions(), $this->get_delete_user_functions() )
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -728,7 +743,7 @@ class Users extends Module {
|
||||
* @todo Refactor to prepare the SQL query before executing it.
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) {
|
||||
global $wpdb;
|
||||
@ -760,7 +775,7 @@ class Users extends Module {
|
||||
$query = "meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0";
|
||||
|
||||
// The $config variable is a list of user IDs to sync.
|
||||
if ( is_array( $config ) ) {
|
||||
if ( is_array( $config ) && ! empty( $config ) ) {
|
||||
$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
|
||||
}
|
||||
|
||||
@ -832,16 +847,17 @@ class Users extends Module {
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param int $user_id ID of the user.
|
||||
* @param int $blog_id ID of the blog.
|
||||
* @param int $user_id ID of the user.
|
||||
* @param int $blog_id ID of the blog.
|
||||
* @param int $reassign ID of the user to whom to reassign posts.
|
||||
*/
|
||||
public function remove_user_from_blog_handler( $user_id, $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public function remove_user_from_blog_handler( $user_id, $blog_id, $reassign = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
// User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114.
|
||||
if ( $this->is_add_new_user_to_blog() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reassigned_user_id = $this->get_reassigned_network_user_id();
|
||||
$reassigned_user_id = $reassign;
|
||||
|
||||
// Note that we are in the context of the blog the user is removed from, see https://github.com/WordPress/WordPress/blob/473e1ba73bc5c18c72d7f288447503713d518790/wp-includes/ms-functions.php#L233.
|
||||
/**
|
||||
@ -868,61 +884,40 @@ class Users extends Module {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're adding an existing user to a blog in this request.
|
||||
* Get the function names that indicate a user is being created.
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @return boolean
|
||||
* @return array
|
||||
*/
|
||||
protected function is_add_user_to_blog() {
|
||||
return $this->is_function_in_backtrace( 'add_user_to_blog' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're removing a user from a blog in this request.
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function is_delete_user() {
|
||||
return $this->is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we're creating a user or adding a new user to a blog.
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function is_create_user() {
|
||||
$functions = array(
|
||||
protected function get_create_user_functions() {
|
||||
return array(
|
||||
'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site.
|
||||
'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site.
|
||||
'wp_insert_user', // Used to suppress jetpack_sync_save_user in save_user_cap_handler and save_user_role_handler when user registered on single site.
|
||||
);
|
||||
|
||||
return $this->is_function_in_backtrace( $functions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ID of the user the removed user's posts are reassigned to (if any).
|
||||
* Get the function names that indicate a user is being added to a blog.
|
||||
*
|
||||
* @return int ID of the user that got reassigned as the author of the posts.
|
||||
* @access protected
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_reassigned_network_user_id() {
|
||||
$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
|
||||
foreach ( $backtrace as $call ) {
|
||||
if (
|
||||
'remove_user_from_blog' === $call['function'] &&
|
||||
3 === count( $call['args'] )
|
||||
) {
|
||||
return $call['args'][2];
|
||||
}
|
||||
}
|
||||
protected function get_add_user_to_blog_functions() {
|
||||
return array( 'add_user_to_blog' );
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Get the function names that indicate a user is being deleted.
|
||||
*
|
||||
* @access protected
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_delete_user_functions() {
|
||||
return array( 'wp_delete_user', 'remove_user_from_blog' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -934,7 +929,7 @@ class Users extends Module {
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_function_in_backtrace( $names ) {
|
||||
$backtrace = debug_backtrace( false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
|
||||
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
|
||||
if ( ! is_array( $names ) ) {
|
||||
$names = array( $names );
|
||||
}
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds WooCommerce HPOS specific data to sync when HPOS is enabled on the site.
|
||||
*/
|
||||
@ -115,7 +119,7 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
public function init_listeners( $callable ) {
|
||||
foreach ( self::get_order_types_to_sync() as $type ) {
|
||||
add_action( "woocommerce_after_{$type}_object_save", $callable );
|
||||
add_filter( "jetpack_sync_before_enqueue_woocommerce_after_{$type}_object_save", array( $this, 'expand_order_object' ) );
|
||||
add_filter( "jetpack_sync_before_enqueue_woocommerce_after_{$type}_object_save", array( $this, 'on_before_enqueue_order_save' ) );
|
||||
}
|
||||
add_action( 'woocommerce_delete_order', $callable );
|
||||
add_action( 'woocommerce_delete_subscription', $callable );
|
||||
@ -144,6 +148,10 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
* @access public
|
||||
*/
|
||||
public function init_before_send() {
|
||||
// Incremental Sync
|
||||
foreach ( self::get_order_types_to_sync() as $type ) {
|
||||
add_filter( "jetpack_sync_before_send_woocommerce_after_{$type}_object_save", array( $this, 'expand_order_object' ) );
|
||||
}
|
||||
// Full sync.
|
||||
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_hpos_orders', array( $this, 'build_full_sync_action_array' ) );
|
||||
}
|
||||
@ -189,7 +197,7 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves multiple orders data by their ID.
|
||||
* Retrieves multiple orders data by their ID. Sorted by ID in descending order.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
@ -209,6 +217,8 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
'type' => self::get_order_types_to_sync( true ),
|
||||
'post_status' => self::get_all_possible_order_status_keys(),
|
||||
'limit' => -1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
@ -256,6 +266,29 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve filtered order data before sending.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param array $args An array with order data.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function expand_order_object( $args ) {
|
||||
if ( empty( $args['id'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$order_object = wc_get_order( $args['id'] );
|
||||
|
||||
if ( ! $order_object instanceof \WC_Abstract_Order ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->filter_order_data( $order_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve order data by its ID.
|
||||
*
|
||||
@ -263,9 +296,12 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
*
|
||||
* @param array $args Order ID.
|
||||
*
|
||||
* @return array
|
||||
* @return array|false
|
||||
*/
|
||||
public function expand_order_object( $args ) {
|
||||
public function on_before_enqueue_order_save( $args ) {
|
||||
// Prevent multiple triggers on a single request.
|
||||
static $processed = array();
|
||||
|
||||
if ( ! is_array( $args ) || ! isset( $args[0] ) ) {
|
||||
return false;
|
||||
}
|
||||
@ -279,7 +315,19 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->filter_order_data( $order_object );
|
||||
$order_id = $order_object->get_id();
|
||||
|
||||
if ( empty( $order_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $processed[ $order_id ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$processed[ $order_id ] = true;
|
||||
|
||||
return array( 'id' => $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -478,7 +526,7 @@ class WooCommerce_HPOS_Orders extends Module {
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- We return all order count for full sync, so confit is not required.
|
||||
global $wpdb;
|
||||
|
||||
@ -0,0 +1,567 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Products sync module.
|
||||
*
|
||||
* @package automattic/jetpack-sync
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
use DateTimeZone;
|
||||
use WC_DateTime;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class to handle sync for WooCommerce Products table.
|
||||
*
|
||||
* Note: This module is currently used for analytics purposes only.
|
||||
*/
|
||||
class WooCommerce_Products extends Module {
|
||||
|
||||
const PRODUCT_POST_TYPES = array( 'product', 'product_variation' );
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
_deprecated_class( 'WooCommerce_Products', '4.24.0', 'Automattic\Jetpack\Sync\Modules\Posts' );
|
||||
// Preprocess action to be sent by Jetpack sync for wp_delete_post.
|
||||
add_action( 'delete_post', array( $this, 'action_wp_delete_post' ), 10, 1 );
|
||||
add_action( 'trashed_post', array( $this, 'action_wp_trash_post' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync module name.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name() {
|
||||
return 'woocommerce_products';
|
||||
}
|
||||
|
||||
/**
|
||||
* The table in the database with the prefix.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function table() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_product_meta_lookup';
|
||||
}
|
||||
|
||||
/**
|
||||
* The id field in the database.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id_field() {
|
||||
return 'product_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* The full sync action name for this module.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function full_sync_action_name() {
|
||||
return 'jetpack_full_sync_woocommerce_products';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WooCommerce Products action listeners.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param callable $callable Action handler callable.
|
||||
*/
|
||||
public function init_listeners( $callable ) {
|
||||
// Listen to product creation and updates - these hooks trigger products table updates
|
||||
add_action( 'woocommerce_new_product', $callable, 10, 1 );
|
||||
add_action( 'woocommerce_update_product', $callable, 10, 1 );
|
||||
|
||||
// Listen to variation creation and updates (they also affect products table)
|
||||
add_action( 'woocommerce_new_product_variation', $callable, 10, 1 );
|
||||
add_action( 'woocommerce_update_product_variation', $callable, 10, 1 );
|
||||
|
||||
// Listen to specific stock update.
|
||||
add_action( 'woocommerce_updated_product_stock', $callable, 10, 1 );
|
||||
|
||||
// Listen to product trashed.
|
||||
add_action( 'jetpack_sync_woocommerce_product_trashed', $callable, 10, 1 );
|
||||
|
||||
// Listen to product deletion via wp_delete_post (more reliable than WC hooks)
|
||||
add_action( 'jetpack_sync_woocommerce_product_deleted', $callable, 10, 1 );
|
||||
|
||||
// Add filters to expand product data before sync
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_product', array( $this, 'expand_product_data' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_product', array( $this, 'expand_product_data' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_product_variation', array( $this, 'expand_product_data' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_product_variation', array( $this, 'expand_product_data' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_updated_product_stock', array( $this, 'expand_product_data' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_woocommerce_product_trashed', array( $this, 'expand_product_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WooCommerce Products action listeners for full sync.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param callable $callable Action handler callable.
|
||||
*/
|
||||
public function init_full_sync_listeners( $callable ) {
|
||||
add_action( 'jetpack_full_sync_woocommerce_products', $callable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the actions that will be sent for this module during a full sync.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return array Full sync actions of this module.
|
||||
*/
|
||||
public function get_full_sync_actions() {
|
||||
return array( 'jetpack_full_sync_woocommerce_products' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module in the sender.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function init_before_send() {
|
||||
// Full sync.
|
||||
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_products', array( $this, 'build_full_sync_action_array' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wp_delete_post action and trigger custom product deletion sync for WooCommerce products.
|
||||
*
|
||||
* @param int $post_id The post ID being deleted.
|
||||
*/
|
||||
public function action_wp_delete_post( $post_id ) {
|
||||
if ( $this->is_a_product_post( $post_id ) ) {
|
||||
/**
|
||||
* Fires when a WooCommerce product is deleted via wp_delete_post.
|
||||
*
|
||||
* @param int $post_id The product ID being deleted.
|
||||
*/
|
||||
do_action( 'jetpack_sync_woocommerce_product_deleted', $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wp_trash_post action and trigger custom product trashed sync for WooCommerce products.
|
||||
*
|
||||
* @param int $post_id The post ID being trashed.
|
||||
*/
|
||||
public function action_wp_trash_post( $post_id ) {
|
||||
if ( $this->is_a_product_post( $post_id ) ) {
|
||||
/**
|
||||
* Fires when a WooCommerce product is trashed via wp_trash_post.
|
||||
*
|
||||
* @param int $post_id The product ID being trashed.
|
||||
*/
|
||||
do_action( 'jetpack_sync_woocommerce_product_trashed', $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand product data to include products table information.
|
||||
*
|
||||
* @param array $args The hook arguments.
|
||||
* @return array $args The hook arguments with expanded data.
|
||||
*/
|
||||
public function expand_product_data( $args ) {
|
||||
if ( empty( $args[0] ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$product_id = $args[0];
|
||||
|
||||
// Get the product data
|
||||
$product_data = $this->get_product_by_ids( array( $product_id ) );
|
||||
|
||||
if ( ! empty( $product_data ) ) {
|
||||
$args[1] = reset( $product_data ); // Get the first (and only) result
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the WooCommerce Products actions for full sync.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @param int $max_items_to_enqueue Maximum number of items to enqueue.
|
||||
* @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
|
||||
* @return array Number of actions enqueued, and next module state.
|
||||
*/
|
||||
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
|
||||
return $this->enqueue_all_ids_as_action(
|
||||
'jetpack_full_sync_woocommerce_products',
|
||||
$this->table(),
|
||||
'product_id',
|
||||
$this->get_where_sql( $config ),
|
||||
$max_items_to_enqueue,
|
||||
$state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an estimated number of actions that will be enqueued.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) {
|
||||
global $wpdb;
|
||||
|
||||
$query = "SELECT count(*) FROM {$this->table()} WHERE " . $this->get_where_sql( $config );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = (int) $wpdb->get_var( $query );
|
||||
|
||||
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of objects by their type and IDs
|
||||
*
|
||||
* @param string $object_type Object type.
|
||||
* @param array $ids IDs of objects to return.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return array|object|WP_Error|null
|
||||
*/
|
||||
public function get_objects_by_id( $object_type, $ids ) {
|
||||
if ( 'product' !== $object_type || empty( $ids ) || ! is_array( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $this->get_product_by_ids( $ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of product objects by their IDs.
|
||||
*
|
||||
* @param array $ids List of product IDs to fetch.
|
||||
* @param string $order Either 'ASC' or 'DESC'.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @return array|object|null
|
||||
*/
|
||||
public function get_product_by_ids( $ids, $order = '' ) {
|
||||
if ( ! is_array( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Make sure the IDs are numeric and are non-zero.
|
||||
$ids = array_filter( array_map( 'intval', $ids ) );
|
||||
|
||||
if ( empty( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$posts = $this->get_product_posts( $ids, $order );
|
||||
$product_types = $this->get_product_types( $ids, $order );
|
||||
|
||||
$products = array();
|
||||
|
||||
// Build base product data from posts.
|
||||
foreach ( $posts as $post ) {
|
||||
$products[ $post->ID ] = array(
|
||||
'product_id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'post_status' => $post->post_status,
|
||||
'slug' => $post->post_name,
|
||||
'date_created' => $this->datetime_to_object( $post->post_date ),
|
||||
'date_modified' => $this->datetime_to_object( $post->post_modified ),
|
||||
);
|
||||
|
||||
$post_type = $post->post_type;
|
||||
// ProductType::VARIATION and ProductType::SIMPLE have only existed since WooCommerce 9.7, so
|
||||
// we can't rely on that existing, but using the strings is probably safe enough.
|
||||
if ( 'product_variation' === $post_type ) {
|
||||
$product_type = 'variation';
|
||||
} elseif ( 'product' === $post_type ) {
|
||||
$product_type = $product_types[ $post->ID ] ?? 'simple';
|
||||
} else {
|
||||
$product_type = null;
|
||||
}
|
||||
$products[ $post->ID ]['type'] = $product_type;
|
||||
}
|
||||
|
||||
// Merge in product meta data.
|
||||
$product_meta_data = $this->get_product_meta_data( $ids, $order );
|
||||
foreach ( $product_meta_data as $meta ) {
|
||||
$product_id = $meta['product_id'];
|
||||
if ( isset( $products[ $product_id ] ) ) {
|
||||
$products[ $product_id ] = array_merge( $products[ $product_id ], $meta );
|
||||
} else {
|
||||
$products[ $product_id ] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
// Add COGS data.
|
||||
$cogs_data = $this->get_product_cogs_data( $ids, $order );
|
||||
foreach ( $cogs_data as $product_id => $cogs_value ) {
|
||||
if ( ! isset( $products[ $product_id ] ) ) {
|
||||
$products[ $product_id ] = array();
|
||||
}
|
||||
$products[ $product_id ]['cogs_amount'] = $cogs_value;
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full sync action object for WooCommerce products.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @param array $args An array with the product data and the previous end.
|
||||
*
|
||||
* @return array An array with the product data and the previous end.
|
||||
*/
|
||||
public function build_full_sync_action_array( $args ) {
|
||||
list( $filtered_product, $previous_end ) = $args;
|
||||
return array(
|
||||
'product' => $filtered_product['objects'],
|
||||
'previous_end' => $previous_end,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the Module Configuration and Status return the next chunk of items to send.
|
||||
*
|
||||
* @param array $config This module Full Sync configuration.
|
||||
* @param array $status This module Full Sync status.
|
||||
* @param int $chunk_size Chunk size.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_next_chunk( $config, $status, $chunk_size ) {
|
||||
$product_ids = parent::get_next_chunk( $config, $status, $chunk_size );
|
||||
|
||||
if ( empty( $product_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Fetch the product data in DESC order for the next chunk logic to work.
|
||||
$product_data = $this->get_product_by_ids( $product_ids, 'DESC' );
|
||||
|
||||
// If no data was fetched, make sure to return the expected structure so that status is updated correctly.
|
||||
if ( empty( $product_data ) ) {
|
||||
return array(
|
||||
'object_ids' => $product_ids,
|
||||
'objects' => array(),
|
||||
);
|
||||
}
|
||||
// Filter the product data based on the maximum size constraints.
|
||||
// We don't have separate metadata, so we pass empty array for metadata.
|
||||
list( $filtered_product_ids, $filtered_product_data, ) = $this->filter_objects_and_metadata_by_size(
|
||||
'product',
|
||||
$product_data,
|
||||
array(), // No separate metadata for products table
|
||||
0, // No individual meta size limit since we don't have separate metadata
|
||||
self::MAX_SIZE_FULL_SYNC
|
||||
);
|
||||
|
||||
return array(
|
||||
'object_ids' => $filtered_product_ids,
|
||||
'objects' => $filtered_product_data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product meta data from the product meta lookup table.
|
||||
*
|
||||
* @param array $ids List of product IDs to fetch.
|
||||
* @param string $order Either 'ASC' or 'DESC'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_product_meta_data( $ids, $order = '' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Prepare the placeholders for the prepared query below.
|
||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||
|
||||
$query = "SELECT * FROM {$this->table()} WHERE product_id IN ( $placeholders )";
|
||||
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
|
||||
$query .= " ORDER BY product_id $order";
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
|
||||
$product_meta_data = $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
|
||||
|
||||
if ( ! is_array( $product_meta_data ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $product_meta_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product data from the posts table.
|
||||
*
|
||||
* @param array $ids List of product IDs to fetch.
|
||||
* @param string $order Either 'ASC' or 'DESC'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_product_posts( $ids, $order = '' ) {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'include' => $ids,
|
||||
'order' => $order,
|
||||
'post_type' => self::PRODUCT_POST_TYPES,
|
||||
'post_status' => array( 'any', 'trash', 'auto-draft' ),
|
||||
'numberposts' => -1, // Get all posts.
|
||||
)
|
||||
);
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product cogs data from the product meta lookup table.
|
||||
*
|
||||
* @param array $ids List of product IDs to fetch.
|
||||
* @param string $order Either 'ASC' or 'DESC'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_product_cogs_data( $ids, $order = '' ) {
|
||||
// @phan-suppress-current-line UnusedPluginSuppression @phan-suppress-next-line PhanUndeclaredClassMethod -- we're checking for the class (around since WooCommerce 7.1) before calling the method (introduced as part of the original class). See also: https://github.com/phan/phan/issues/1204
|
||||
$is_cogs_enabled = class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) && \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'cost_of_goods_sold' );
|
||||
|
||||
if ( ! $is_cogs_enabled ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Prepare the placeholders for the prepared query below.
|
||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||
|
||||
$query = "
|
||||
SELECT post_id, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE post_id IN ( $placeholders )
|
||||
AND meta_key = '_cogs_total_value'
|
||||
";
|
||||
|
||||
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
|
||||
$query .= " ORDER BY post_id $order";
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
|
||||
|
||||
if ( ! is_array( $results ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$product_cogs_data = array();
|
||||
foreach ( $results as $result ) {
|
||||
$cogs_value = '' === $result['meta_value'] ? null : (float) $result['meta_value'];
|
||||
$product_cogs_data[ $result['post_id'] ] = $cogs_value;
|
||||
}
|
||||
|
||||
return $product_cogs_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product types for multiple product IDs in bulk.
|
||||
*
|
||||
* @param array $ids List of product IDs to fetch types for.
|
||||
* @param string $order Either 'ASC' or 'DESC'.
|
||||
*
|
||||
* @return array Array of product_id => product_type mapping.
|
||||
*/
|
||||
private function get_product_types( $ids, $order = '' ) {
|
||||
if ( empty( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Bulk load term relationships and term data
|
||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||
$query = "
|
||||
SELECT tr.object_id, t.name
|
||||
FROM {$wpdb->term_relationships} tr
|
||||
INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
|
||||
INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
|
||||
WHERE tr.object_id IN ( $placeholders )
|
||||
AND tt.taxonomy = 'product_type'
|
||||
";
|
||||
|
||||
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
|
||||
$query .= " ORDER BY tr.object_id $order";
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $ids ) );
|
||||
|
||||
if ( ! is_array( $results ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$product_types = array();
|
||||
foreach ( $results as $result ) {
|
||||
$product_types[ $result->object_id ] = sanitize_title( $result->name );
|
||||
}
|
||||
|
||||
return $product_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the WC_DateTime objects to stdClass objects to ensure they are properly encoded.
|
||||
*
|
||||
* @param WC_DateTime|mixed $wc_datetime The datetime object.
|
||||
* @param bool $utc Whether to convert to UTC.
|
||||
* @return object|null
|
||||
*/
|
||||
private function datetime_to_object( $wc_datetime, $utc = false ) {
|
||||
if ( is_string( $wc_datetime ) ) {
|
||||
$wc_datetime = new WC_DateTime( $wc_datetime, new DateTimeZone( wc_timezone_string() ) );
|
||||
}
|
||||
|
||||
if ( is_a( $wc_datetime, 'WC_DateTime' ) ) {
|
||||
if ( $utc ) {
|
||||
$wc_datetime->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
} else {
|
||||
$wc_datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
|
||||
}
|
||||
return (object) (array) $wc_datetime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the post is a product post.
|
||||
*
|
||||
* @param int $post_id The post ID to check.
|
||||
* @return bool True if the post is a product post, false otherwise.
|
||||
*/
|
||||
private function is_a_product_post( $post_id ) {
|
||||
$post_type = get_post_type( $post_id );
|
||||
return in_array( $post_type, self::PRODUCT_POST_TYPES, true );
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,10 @@ namespace Automattic\Jetpack\Sync\Modules;
|
||||
use WC_Order;
|
||||
use WP_Error;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for WooCommerce.
|
||||
*/
|
||||
@ -125,7 +129,6 @@ class WooCommerce extends Module {
|
||||
add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
|
||||
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
|
||||
add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'filter_order_item' ) );
|
||||
add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
|
||||
|
||||
// Blacklist Action Scheduler comment types.
|
||||
@ -166,10 +169,10 @@ class WooCommerce extends Module {
|
||||
|
||||
// Order items.
|
||||
add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
|
||||
add_action( 'woocommerce_update_order_item', $callable, 10, 4 );
|
||||
add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
|
||||
add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
|
||||
$this->init_listeners_for_meta_type( 'order_item', $callable );
|
||||
$this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
|
||||
|
||||
// Payment tokens.
|
||||
add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
|
||||
@ -182,6 +185,7 @@ class WooCommerce extends Module {
|
||||
add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
|
||||
|
||||
// Tax rates.
|
||||
// These are ignored on WP.com: tax items are derived from order data via wc_order_tax_lookup, which isn’t present there.
|
||||
add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
|
||||
add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
|
||||
add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
|
||||
@ -238,6 +242,38 @@ class WooCommerce extends Module {
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for filtering out non-whitelisted order item meta.
|
||||
*
|
||||
* @since 4.22.3
|
||||
*
|
||||
* @param array $args Hook arguments.
|
||||
* @return array|false False if not whitelisted, the original hook args otherwise.
|
||||
*/
|
||||
public function filter_meta( $args ) {
|
||||
if (
|
||||
! empty( $args[2] ) && $this->is_whitelisted_order_item_meta( $args[2] )
|
||||
) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an order item meta key is whitelisted for sync.
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @since 4.22.3
|
||||
*
|
||||
* @param string $meta_key Order item meta key.
|
||||
* @return bool True if whitelisted.
|
||||
*/
|
||||
public function is_whitelisted_order_item_meta( $meta_key ) {
|
||||
return is_string( $meta_key ) && in_array( $meta_key, self::$order_item_meta_whitelist, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the order item ids to be removed and send them as one action
|
||||
*
|
||||
@ -292,15 +328,13 @@ class WooCommerce extends Module {
|
||||
*
|
||||
* @access public
|
||||
*
|
||||
* @todo Refactor table name to use a $wpdb->prepare placeholder.
|
||||
*
|
||||
* @param int $order_item_id Order item ID.
|
||||
* @return object Order item.
|
||||
*/
|
||||
public function build_order_item( $order_item_id ) {
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) );
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct database access is intentional; caching is not required for this query.
|
||||
return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE order_item_id = %d', $this->order_item_table_name, $order_item_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -325,7 +359,7 @@ class WooCommerce extends Module {
|
||||
* @todo Refactor the SQL query to use $wpdb->prepare().
|
||||
*
|
||||
* @param array $config Full sync configuration for this sync module.
|
||||
* @return array Number of items yet to be enqueued.
|
||||
* @return int Number of items yet to be enqueued.
|
||||
*/
|
||||
public function estimate_full_sync_actions( $config ) {
|
||||
global $wpdb;
|
||||
@ -467,8 +501,12 @@ class WooCommerce extends Module {
|
||||
'woocommerce_api_enabled',
|
||||
'woocommerce_allow_tracking',
|
||||
'woocommerce_task_list_hidden',
|
||||
'woocommerce_onboarding_profile',
|
||||
'woocommerce_cod_settings',
|
||||
'woocommerce_store_address',
|
||||
'woocommerce_store_address_2',
|
||||
'woocommerce_store_city',
|
||||
'woocommerce_store_postcode',
|
||||
'woocommerce_admin_install_timestamp',
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
|
||||
namespace Automattic\Jetpack\Sync\Modules;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle sync for WP_Super_Cache.
|
||||
*/
|
||||
|
||||
@ -13,6 +13,10 @@ use Automattic\Jetpack\Sync\Modules;
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle Table Checksums for the User Meta table.
|
||||
*/
|
||||
|
||||
@ -9,6 +9,10 @@ namespace Automattic\Jetpack\Sync\Replicastore;
|
||||
|
||||
use Exception;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle Table Checksums for the Users table.
|
||||
*/
|
||||
|
||||
@ -213,18 +213,14 @@ class Table_Checksum {
|
||||
'range_field' => 'comment_ID',
|
||||
'key_fields' => array( 'comment_ID' ),
|
||||
'checksum_fields' => array( 'comment_date_gmt' ),
|
||||
'filter_values' => array(
|
||||
'comment_type' => array(
|
||||
'operator' => 'IN',
|
||||
'values' => apply_filters(
|
||||
'jetpack_sync_whitelisted_comment_types',
|
||||
array( '', 'comment', 'trackback', 'pingback', 'review' )
|
||||
'filter_values' => array_merge(
|
||||
Sync\Settings::get_allowed_comment_types_structured(),
|
||||
array(
|
||||
'comment_approved' => array(
|
||||
'operator' => 'NOT IN',
|
||||
'values' => array( 'spam' ),
|
||||
),
|
||||
),
|
||||
'comment_approved' => array(
|
||||
'operator' => 'NOT IN',
|
||||
'values' => array( 'spam' ),
|
||||
),
|
||||
)
|
||||
),
|
||||
'is_table_enabled_callback' => function () {
|
||||
return false !== Sync\Modules::get_module( 'comments' );
|
||||
@ -289,6 +285,21 @@ class Table_Checksum {
|
||||
),
|
||||
'links' => $wpdb->links, // TODO describe in the array format or add exceptions.
|
||||
'options' => $wpdb->options, // TODO describe in the array format or add exceptions.
|
||||
'wc_product_lookup' => array( // wc_product_lookup is a table in the cache database
|
||||
'table' => $wpdb->posts,
|
||||
'range_field' => 'ID',
|
||||
'key_fields' => array( 'ID' ),
|
||||
'checksum_fields' => array( 'post_modified_gmt' ),
|
||||
'filter_values' => array(
|
||||
'post_type' => array(
|
||||
'operator' => 'IN',
|
||||
'values' => array( 'product', 'product_variation' ),
|
||||
),
|
||||
),
|
||||
'is_table_enabled_callback' => function () {
|
||||
return false !== Sync\Modules::get_module( 'woocommerce_products' );
|
||||
},
|
||||
),
|
||||
'woocommerce_order_items' => array(
|
||||
'table' => "{$wpdb->prefix}woocommerce_order_items",
|
||||
'range_field' => 'order_item_id',
|
||||
@ -306,7 +317,9 @@ class Table_Checksum {
|
||||
'parent_table' => 'woocommerce_order_items',
|
||||
'parent_join_field' => 'order_item_id',
|
||||
'table_join_field' => 'order_item_id',
|
||||
'is_table_enabled_callback' => 'Automattic\Jetpack\Sync\Replicastore\Table_Checksum::enable_woocommerce_tables',
|
||||
'is_table_enabled_callback' => function () {
|
||||
return false !== Sync\Modules::get_module( 'meta' ) && self::enable_woocommerce_tables();
|
||||
},
|
||||
),
|
||||
'wc_orders' => array(
|
||||
'table' => "{$wpdb->prefix}wc_orders",
|
||||
@ -398,14 +411,14 @@ class Table_Checksum {
|
||||
protected function prepare_fields( $table_configuration ) {
|
||||
$this->key_fields = $table_configuration['key_fields'];
|
||||
$this->range_field = $table_configuration['range_field'];
|
||||
$this->checksum_fields = isset( $table_configuration['checksum_fields'] ) ? $table_configuration['checksum_fields'] : array();
|
||||
$this->checksum_text_fields = isset( $table_configuration['checksum_text_fields'] ) ? $table_configuration['checksum_text_fields'] : array();
|
||||
$this->filter_values = isset( $table_configuration['filter_values'] ) ? $table_configuration['filter_values'] : null;
|
||||
$this->checksum_fields = $table_configuration['checksum_fields'] ?? array();
|
||||
$this->checksum_text_fields = $table_configuration['checksum_text_fields'] ?? array();
|
||||
$this->filter_values = $table_configuration['filter_values'] ?? null;
|
||||
$this->additional_filter_sql = ! empty( $table_configuration['filter_sql'] ) ? $table_configuration['filter_sql'] : '';
|
||||
$this->parent_table = isset( $table_configuration['parent_table'] ) ? $table_configuration['parent_table'] : null;
|
||||
$this->parent_join_field = isset( $table_configuration['parent_join_field'] ) ? $table_configuration['parent_join_field'] : $table_configuration['range_field'];
|
||||
$this->table_join_field = isset( $table_configuration['table_join_field'] ) ? $table_configuration['table_join_field'] : $table_configuration['range_field'];
|
||||
$this->is_table_enabled_callback = isset( $table_configuration['is_table_enabled_callback'] ) ? $table_configuration['is_table_enabled_callback'] : false;
|
||||
$this->parent_table = $table_configuration['parent_table'] ?? null;
|
||||
$this->parent_join_field = $table_configuration['parent_join_field'] ?? $table_configuration['range_field'];
|
||||
$this->table_join_field = $table_configuration['table_join_field'] ?? $table_configuration['range_field'];
|
||||
$this->is_table_enabled_callback = $table_configuration['is_table_enabled_callback'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -654,7 +667,10 @@ class Table_Checksum {
|
||||
$filter_stamenet = $this->build_filter_statement( $range_from, $range_to, $filter_values );
|
||||
|
||||
$join_statement = '';
|
||||
if ( $this->parent_table ) {
|
||||
// On WPCOM the checksum comparison does not use the parent table INNER JOIN.
|
||||
// WPCOM sets parent_table in its config solely for the count optimization in
|
||||
// get_range_edges(), so we skip the JOIN to avoid query differences.
|
||||
if ( $this->parent_table && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
|
||||
$parent_table_obj = new Table_Checksum( $this->parent_table );
|
||||
$parent_filter_query = $parent_table_obj->build_filter_statement( null, null, null, 'parent_table' );
|
||||
|
||||
@ -724,19 +740,26 @@ class Table_Checksum {
|
||||
|
||||
$this->validate_fields( array( $this->range_field ) );
|
||||
|
||||
// Performance :: When getting the postmeta range we do not want to filter by the whitelist.
|
||||
// The reason for this is that it leads to a non-performant query that can timeout.
|
||||
// Instead lets get the range based on posts regardless of meta.
|
||||
// Performance :: For meta tables (postmeta, commentmeta, termmeta, woocommerce_order_itemmeta)
|
||||
// we strip the filter_values (e.g. meta_key whitelist) when building the range edges query.
|
||||
// These filters cause non-performant queries that can timeout on large tables.
|
||||
// The actual data filtering happens during checksum calculation — via the filter_values
|
||||
// WHERE clause and, when enabled, the parent table INNER JOIN.
|
||||
$is_meta_table = in_array(
|
||||
$this->table,
|
||||
array( $wpdb->postmeta, $wpdb->commentmeta, $wpdb->termmeta, "{$wpdb->prefix}woocommerce_order_itemmeta" ),
|
||||
true
|
||||
);
|
||||
$filter_values = $this->filter_values;
|
||||
if ( $wpdb->postmeta === $this->table ) {
|
||||
if ( $is_meta_table ) {
|
||||
$this->filter_values = null;
|
||||
}
|
||||
|
||||
// `trim()` to make sure we don't add the statement if it's empty.
|
||||
$filters = trim( $this->build_filter_statement( $range_from, $range_to ) );
|
||||
|
||||
// Reset Post meta filter.
|
||||
if ( $wpdb->postmeta === $this->table ) {
|
||||
// Restore filter values.
|
||||
if ( $is_meta_table ) {
|
||||
$this->filter_values = $filter_values;
|
||||
}
|
||||
|
||||
@ -766,6 +789,33 @@ class Table_Checksum {
|
||||
* If `$limit` is not specified, we can directly use the table.
|
||||
*/
|
||||
if ( ! $limit ) {
|
||||
// For tables that would use COUNT(DISTINCT), avoid the expensive full table scan
|
||||
// by using the parent table's count instead. Only for full-table calls — sub-range
|
||||
// calls need the actual COUNT(DISTINCT) scoped to the range, and those are cheap
|
||||
// because the WHERE clause limits the scan.
|
||||
if ( $distinct_count && null === $range_from && null === $range_to ) {
|
||||
$parent_count = $this->get_parent_table_count();
|
||||
if ( (int) $parent_count > 0 ) {
|
||||
$min_max_query = "
|
||||
SELECT
|
||||
MIN({$this->range_field}) as min_range,
|
||||
MAX({$this->range_field}) as max_range
|
||||
FROM
|
||||
{$this->table}
|
||||
{$filter_statement}
|
||||
";
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$result = $wpdb->get_row( $min_max_query, ARRAY_A );
|
||||
|
||||
if ( $result && is_array( $result ) ) {
|
||||
$result['item_count'] = $parent_count;
|
||||
self::$range_edges_cache[ $this->table ] = $result;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query .= "
|
||||
{$this->table}
|
||||
{$filter_statement}
|
||||
@ -798,9 +848,93 @@ class Table_Checksum {
|
||||
throw new Exception( 'Unable to get range edges' );
|
||||
}
|
||||
|
||||
// Cache full-range results so child meta tables can reuse the parent's count.
|
||||
// Only cache when no range constraints — sub-range counts would pollute the cache.
|
||||
if ( ! $limit && null === $range_from && null === $range_to ) {
|
||||
self::$range_edges_cache[ $this->table ] = $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static cache for range edge results, keyed by table name.
|
||||
*
|
||||
* When checksum_all() processes tables sequentially, the parent table's
|
||||
* get_range_edges() result is cached so child tables can reuse the
|
||||
* item_count without re-querying.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $range_edges_cache = array();
|
||||
|
||||
/**
|
||||
* Reset the static range edges cache.
|
||||
*
|
||||
* Should be called when the underlying data changes and cached
|
||||
* counts may be stale (e.g. between test runs).
|
||||
*/
|
||||
public static function reset_range_edges_cache() {
|
||||
self::$range_edges_cache = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the row count from the parent table as an approximate item count.
|
||||
*
|
||||
* For tables with compound keys or non-unique range fields, COUNT(DISTINCT range_field)
|
||||
* causes expensive full table scans. Since item_count is only used for bucket sizing
|
||||
* in checksum_histogram(), the parent table's row count is an acceptable approximation.
|
||||
* In typical cases the parent count >= the distinct child count, producing slightly
|
||||
* more (smaller) buckets. The caller guards against a zero parent count (e.g. orphaned
|
||||
* child rows) by falling back to the original COUNT(DISTINCT) query.
|
||||
*
|
||||
* Returns false when the parent table's count is not a reliable proxy (e.g.
|
||||
* term_taxonomy, whose count does not correlate with distinct range_field values
|
||||
* in terms, termmeta, or term_relationships).
|
||||
*
|
||||
* Uses a static cache so that if the parent table was already processed
|
||||
* (e.g. posts before postmeta in checksum_all), no additional query is needed.
|
||||
*
|
||||
* @return int|false The parent table row count, or false if not applicable.
|
||||
*/
|
||||
private function get_parent_table_count() {
|
||||
if ( ! $this->parent_table ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// term_taxonomy's count is not a reliable proxy for the distinct range_field
|
||||
// values in terms, termmeta, or term_relationships.
|
||||
if ( 'term_taxonomy' === $this->parent_table ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$parent_table_obj = new Table_Checksum( $this->parent_table );
|
||||
} catch ( Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check static cache first — the parent may have been queried already
|
||||
// (e.g. posts processed before postmeta in checksum_all).
|
||||
if ( isset( self::$range_edges_cache[ $parent_table_obj->table ] ) ) {
|
||||
return (int) self::$range_edges_cache[ $parent_table_obj->table ]['item_count'];
|
||||
}
|
||||
|
||||
// Query the parent table's range edges. For single-key parent tables this is
|
||||
// a simple COUNT (no DISTINCT), so it's fast.
|
||||
try {
|
||||
$parent_range = $parent_table_obj->get_range_edges();
|
||||
|
||||
if ( is_array( $parent_range ) && isset( $parent_range['item_count'] ) ) {
|
||||
return (int) $parent_range['item_count'];
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch ( Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the results to have key/checksum format.
|
||||
*
|
||||
|
||||
@ -67,35 +67,37 @@ class Queue_Storage_Options {
|
||||
* Fetch items from the queue.
|
||||
*
|
||||
* @param int|null $item_count How many items to fetch from the queue.
|
||||
* The parameter is null-able, if no limit on the amount of items.
|
||||
* Null for no limit.
|
||||
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
|
||||
* Any other value will be treated as 'ASC'.
|
||||
*
|
||||
* @return array|object|\stdClass[]|null
|
||||
* @return array|object|null Array of result objects on success, or null on failure.
|
||||
*/
|
||||
public function fetch_items( $item_count ) {
|
||||
public function fetch_items( $item_count, $order = 'ASC' ) {
|
||||
global $wpdb;
|
||||
|
||||
// TODO make it more simple for the $item_count
|
||||
$order = 'DESC' === $order ? 'DESC' : 'ASC';
|
||||
|
||||
$sql_order = "ORDER BY option_name {$order}";
|
||||
|
||||
$sql = "SELECT option_name AS id, option_value AS value
|
||||
FROM $wpdb->options
|
||||
WHERE option_name LIKE %s
|
||||
{$sql_order}";
|
||||
|
||||
$params = array( "jpsq_{$this->queue_id}-%" );
|
||||
|
||||
if ( $item_count ) {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
|
||||
"jpsq_{$this->queue_id}-%",
|
||||
$item_count
|
||||
),
|
||||
OBJECT
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC",
|
||||
"jpsq_{$this->queue_id}-%"
|
||||
),
|
||||
OBJECT
|
||||
);
|
||||
$sql .= ' LIMIT %d';
|
||||
$params[] = $item_count;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare( $sql, $params ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
OBJECT
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
@ -228,55 +228,38 @@ class Queue_Storage_Table {
|
||||
* Fetch items from the queue.
|
||||
*
|
||||
* @param int|null $item_count How many items to fetch from the queue.
|
||||
* The parameter is null-able, if no limit on the amount of items.
|
||||
* Null for no limit.
|
||||
* @param string $order Sort direction for the items. Accepts 'ASC' or 'DESC'.
|
||||
* Any other value will be treated as 'ASC'.
|
||||
*
|
||||
* @return object[]|null
|
||||
* @return array|object|null Array of result objects on success, or null on failure.
|
||||
*/
|
||||
public function fetch_items( $item_count ) {
|
||||
public function fetch_items( $item_count, $order = 'ASC' ) {
|
||||
global $wpdb;
|
||||
|
||||
/**
|
||||
* Ignoring the linting warning, as there's still no placeholder replacement for DB field name,
|
||||
* in this case this is `$this->table_name`
|
||||
*/
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$order = ( 'DESC' === $order ) ? 'DESC' : 'ASC';
|
||||
$sql_order = "ORDER BY event_id {$order}";
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
event_id AS id,
|
||||
event_payload AS value
|
||||
FROM {$this->table_name}
|
||||
WHERE queue_id LIKE %s
|
||||
{$sql_order}
|
||||
";
|
||||
|
||||
$params = array( $this->queue_id );
|
||||
|
||||
// TODO make it more simple for the $item_count
|
||||
if ( $item_count ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
event_id AS id,
|
||||
event_payload AS value
|
||||
FROM {$this->table_name}
|
||||
WHERE queue_id LIKE %s
|
||||
ORDER BY event_id ASC
|
||||
LIMIT %d
|
||||
",
|
||||
$this->queue_id,
|
||||
$item_count
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT
|
||||
event_id AS id,
|
||||
event_payload AS value
|
||||
FROM {$this->table_name}
|
||||
WHERE queue_id LIKE %s
|
||||
ORDER BY event_id ASC
|
||||
",
|
||||
$this->queue_id
|
||||
)
|
||||
);
|
||||
$sql .= ' LIMIT %d';
|
||||
$params[] = $item_count;
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$items = $wpdb->get_results(
|
||||
$wpdb->prepare( $sql, $params ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user