Added login request

This commit is contained in:
Nedim Uka
2018-06-20 18:03:43 +02:00
parent 4e52521fae
commit 593b445a21
4716 changed files with 1218265 additions and 57 deletions

View File

@@ -0,0 +1,464 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php';
/**
* The role of this class is to hook the Sync subsystem into WordPress - when to listen for actions,
* when to send, when to perform a full sync, etc.
*
* It also binds the action to send data to WPCOM to Jetpack's XMLRPC client object.
*/
class Jetpack_Sync_Actions {
static $sender = null;
static $listener = null;
const DEFAULT_SYNC_CRON_INTERVAL_NAME = 'jetpack_sync_interval';
const DEFAULT_SYNC_CRON_INTERVAL_VALUE = 300; // 5 * MINUTE_IN_SECONDS;
static function init() {
// everything below this point should only happen if we're a valid sync site
if ( ! self::sync_allowed() ) {
return;
}
if ( self::sync_via_cron_allowed() ) {
self::init_sync_cron_jobs();
} else if ( wp_next_scheduled( 'jetpack_sync_cron' ) ) {
self::clear_sync_cron_jobs();
}
// When importing via cron, do not sync
add_action( 'wp_cron_importer_hook', array( __CLASS__, 'set_is_importing_true' ), 1 );
// Sync connected user role changes to .com
require_once dirname( __FILE__ ) . '/class.jetpack-sync-users.php';
// publicize filter to prevent publicizing blacklisted post types
add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 );
/**
* Fires on every request before default loading sync listener code.
* Return false to not load sync listener code that monitors common
* WP actions to be serialized.
*
* By default this returns true for cron jobs, non-GET-requests, or requests where the
* user is logged-in.
*
* @since 4.2.0
*
* @param bool should we load sync listener code for this request
*/
if ( apply_filters( 'jetpack_sync_listener_should_load', true ) ) {
self::initialize_listener();
}
add_action( 'init', array( __CLASS__, 'add_sender_shutdown' ), 90 );
}
static function add_sender_shutdown() {
/**
* Fires on every request before default loading sync sender code.
* Return false to not load sync sender code that serializes pending
* data and sends it to WPCOM for processing.
*
* By default this returns true for cron jobs, POST requests, admin requests, or requests
* by users who can manage_options.
*
* @since 4.2.0
*
* @param bool should we load sync sender code for this request
*/
if ( apply_filters( 'jetpack_sync_sender_should_load',
(
( isset( $_SERVER["REQUEST_METHOD"] ) && 'POST' === $_SERVER['REQUEST_METHOD'] )
||
current_user_can( 'manage_options' )
||
is_admin()
||
defined( 'PHPUNIT_JETPACK_TESTSUITE' )
)
) ) {
self::initialize_sender();
add_action( 'shutdown', array( self::$sender, 'do_sync' ) );
add_action( 'shutdown', array( self::$sender, 'do_full_sync' ) );
}
}
static function sync_allowed() {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
return ( ! Jetpack_Sync_Settings::get_setting( 'disable' )
&& ( doing_action( 'jetpack_user_authorized' ) || Jetpack::is_active() )
&& ! ( Jetpack::is_development_mode() || Jetpack::is_staging_site() ) )
|| defined( 'PHPUNIT_JETPACK_TESTSUITE' );
}
static function sync_via_cron_allowed() {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
return ( Jetpack_Sync_Settings::get_setting( 'sync_via_cron' ) );
}
static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
if ( in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) {
return false;
}
return $should_publicize;
}
static function set_is_importing_true() {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
Jetpack_Sync_Settings::set_importing( true );
}
static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-functions.php';
Jetpack::load_xml_rpc_client();
$query_args = array(
'sync' => '1', // add an extra parameter to the URL so we can tell it's a sync action
'codec' => $codec_name, // send the name of the codec used to encode the data
'timestamp' => $sent_timestamp, // send current server time so we can compensate for clock differences
'queue' => $queue_id, // sync or full_sync
'home' => Jetpack_Sync_Functions::home_url(), // Send home url option to check for Identity Crisis server-side
'siteurl' => Jetpack_Sync_Functions::site_url(), // Send siteurl option to check for Identity Crisis server-side
'cd' => sprintf( '%.4f', $checkout_duration), // Time spent retrieving queue items from the DB
'pd' => sprintf( '%.4f', $preprocess_duration), // Time spent converting queue items into data to send
);
// Has the site opted in to IDC mitigation?
if ( Jetpack::sync_idc_optin() ) {
$query_args['idc'] = true;
}
if ( Jetpack_Options::get_option( 'migrate_for_idc', false ) ) {
$query_args['migrate_for_idc'] = true;
}
$query_args['timeout'] = Jetpack_Sync_Settings::is_doing_cron() ? 30 : 15;
/**
* Filters query parameters appended to the Sync request URL sent to WordPress.com.
*
* @since 4.7.0
*
* @param array $query_args associative array of query parameters.
*/
$query_args = apply_filters( 'jetpack_sync_send_data_query_args', $query_args );
$url = add_query_arg( $query_args, Jetpack::xmlrpc_api_url() );
$rpc = new Jetpack_IXR_Client( array(
'url' => $url,
'user_id' => JETPACK_MASTER_USER,
'timeout' => $query_args['timeout'],
) );
$result = $rpc->query( 'jetpack.syncActions', $data );
if ( ! $result ) {
return $rpc->get_jetpack_error();
}
$response = $rpc->getResponse();
// Check if WordPress.com IDC mitigation blocked the sync request
if ( is_array( $response ) && isset( $response['error_code'] ) ) {
$error_code = $response['error_code'];
$allowed_idc_error_codes = array(
'jetpack_url_mismatch',
'jetpack_home_url_mismatch',
'jetpack_site_url_mismatch'
);
if ( in_array( $error_code, $allowed_idc_error_codes ) ) {
Jetpack_Options::update_option(
'sync_error_idc',
Jetpack::get_sync_error_idc_option( $response )
);
}
return new WP_Error(
'sync_error_idc',
esc_html__( 'Sync has been blocked from WordPress.com because it would cause an identity crisis', 'jetpack' )
);
}
return $response;
}
static function do_initial_sync() {
// Lets not sync if we are not suppose to.
if ( ! self::sync_allowed() ) {
return false;
}
$initial_sync_config = array(
'options' => true,
'functions' => true,
'constants' => true,
'users' => array( get_current_user_id() ),
);
if ( is_multisite() ) {
$initial_sync_config['network_options'] = true;
}
self::do_full_sync( $initial_sync_config );
}
static function do_full_sync( $modules = null ) {
if ( ! self::sync_allowed() ) {
return false;
}
$full_sync_module = Jetpack_Sync_Modules::get_module( 'full-sync' );
if ( ! $full_sync_module ) {
return false;
}
self::initialize_listener();
$full_sync_module->start( $modules );
return true;
}
static function jetpack_cron_schedule( $schedules ) {
if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) {
$schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array(
'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE,
'display' => sprintf(
esc_html( _n( 'Every minute', 'Every %d minutes', intval( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 ), 'jetpack' ) ),
intval( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 )
)
);
}
return $schedules;
}
static function do_cron_sync() {
self::do_cron_sync_by_type( 'sync' );
}
static function do_cron_full_sync() {
self::do_cron_sync_by_type( 'full_sync' );
}
/**
* Try to send actions until we run out of things to send,
* or have to wait more than 15s before sending again,
* or we hit a lock or some other sending issue
*
* @param string $type Sync type. Can be `sync` or `full_sync`.
*/
static function do_cron_sync_by_type( $type ) {
if ( ! self::sync_allowed() || ( 'sync' !== $type && 'full_sync' !== $type ) ) {
return;
}
self::initialize_sender();
$time_limit = Jetpack_Sync_Settings::get_setting( 'cron_sync_time_limit' );
$start_time = time();
do {
$next_sync_time = self::$sender->get_next_sync_time( $type );
if ( $next_sync_time ) {
$delay = $next_sync_time - time() + 1;
if ( $delay > 15 ) {
break;
} elseif ( $delay > 0 ) {
sleep( $delay );
}
}
$result = 'full_sync' === $type ? self::$sender->do_full_sync() : self::$sender->do_sync();
} while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
}
static function initialize_listener() {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php';
self::$listener = Jetpack_Sync_Listener::get_instance();
}
static function initialize_sender() {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-sender.php';
self::$sender = Jetpack_Sync_Sender::get_instance();
// bind the sending process
add_filter( 'jetpack_sync_send_data', array( __CLASS__, 'send_data' ), 10, 6 );
}
static function initialize_woocommerce() {
if ( false === class_exists( 'WooCommerce' ) ) {
return;
}
add_filter( 'jetpack_sync_modules', array( 'Jetpack_Sync_Actions', 'add_woocommerce_sync_module' ) );
}
static function add_woocommerce_sync_module( $sync_modules ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-woocommerce.php';
$sync_modules[] = 'Jetpack_Sync_Module_WooCommerce';
return $sync_modules;
}
static function initialize_wp_super_cache() {
if ( false === function_exists( 'wp_cache_is_enabled' ) ) {
return;
}
add_filter( 'jetpack_sync_modules', array( 'Jetpack_Sync_Actions', 'add_wp_super_cache_sync_module' ) );
}
static function add_wp_super_cache_sync_module( $sync_modules ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-wp-super-cache.php';
$sync_modules[] = 'Jetpack_Sync_Module_WP_Super_Cache';
return $sync_modules;
}
static function sanitize_filtered_sync_cron_schedule( $schedule ) {
$schedule = sanitize_key( $schedule );
$schedules = wp_get_schedules();
// Make sure that the schedule has actually been registered using the `cron_intervals` filter.
if ( isset( $schedules[ $schedule ] ) ) {
return $schedule;
}
return self::DEFAULT_SYNC_CRON_INTERVAL_NAME;
}
static function get_start_time_offset( $schedule = '', $hook = '' ) {
$start_time_offset = is_multisite()
? mt_rand( 0, ( 2 * self::DEFAULT_SYNC_CRON_INTERVAL_VALUE ) )
: 0;
/**
* Allows overriding the offset that the sync cron jobs will first run. This can be useful when scheduling
* cron jobs across multiple sites in a network.
*
* @since 4.5
*
* @param int $start_time_offset
* @param string $hook
* @param string $schedule
*/
return intval( apply_filters(
'jetpack_sync_cron_start_time_offset',
$start_time_offset,
$hook,
$schedule
) );
}
static function maybe_schedule_sync_cron( $schedule, $hook ) {
if ( ! $hook ) {
return;
}
$schedule = self::sanitize_filtered_sync_cron_schedule( $schedule );
$start_time = time() + self::get_start_time_offset( $schedule, $hook );
if ( ! wp_next_scheduled( $hook ) ) {
// Schedule a job to send pending queue items once a minute
wp_schedule_event( $start_time, $schedule, $hook );
} else if ( $schedule != wp_get_schedule( $hook ) ) {
// If the schedule has changed, update the schedule
wp_clear_scheduled_hook( $hook );
wp_schedule_event( $start_time, $schedule, $hook );
}
}
static function clear_sync_cron_jobs() {
wp_clear_scheduled_hook( 'jetpack_sync_cron' );
wp_clear_scheduled_hook( 'jetpack_sync_full_cron' );
}
static function init_sync_cron_jobs() {
add_filter( 'cron_schedules', array( __CLASS__, 'jetpack_cron_schedule' ) );
add_action( 'jetpack_sync_cron', array( __CLASS__, 'do_cron_sync' ) );
add_action( 'jetpack_sync_full_cron', array( __CLASS__, 'do_cron_full_sync' ) );
/**
* Allows overriding of the default incremental sync cron schedule which defaults to once every 5 minutes.
*
* @since 4.3.2
*
* @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME
*/
$incremental_sync_cron_schedule = apply_filters( 'jetpack_sync_incremental_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME );
self::maybe_schedule_sync_cron( $incremental_sync_cron_schedule, 'jetpack_sync_cron' );
/**
* Allows overriding of the full sync cron schedule which defaults to once every 5 minutes.
*
* @since 4.3.2
*
* @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME
*/
$full_sync_cron_schedule = apply_filters( 'jetpack_sync_full_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME );
self::maybe_schedule_sync_cron( $full_sync_cron_schedule, 'jetpack_sync_full_cron' );
}
static function cleanup_on_upgrade( $new_version = null, $old_version = null ) {
if ( wp_next_scheduled( 'jetpack_sync_send_db_checksum' ) ) {
wp_clear_scheduled_hook( 'jetpack_sync_send_db_checksum' );
}
$is_new_sync_upgrade = version_compare( $old_version, '4.2', '>=' );
if ( ! empty( $old_version ) && $is_new_sync_upgrade && version_compare( $old_version, '4.5', '<' ) ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
self::clear_sync_cron_jobs();
Jetpack_Sync_Settings::update_settings( array(
'render_filtered_content' => Jetpack_Sync_Defaults::$default_render_filtered_content
) );
}
}
static function get_sync_status() {
self::initialize_sender();
$sync_module = Jetpack_Sync_Modules::get_module( 'full-sync' );
$queue = self::$sender->get_sync_queue();
$full_queue = self::$sender->get_full_sync_queue();
$cron_timestamps = array_keys( _get_cron_array() );
$next_cron = $cron_timestamps[0] - time();
$full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array();
return array_merge(
$full_sync_status,
array(
'cron_size' => count( $cron_timestamps ),
'next_cron' => $next_cron,
'queue_size' => $queue->size(),
'queue_lag' => $queue->lag(),
'queue_next_sync' => ( self::$sender->get_next_sync_time( 'sync' ) - microtime( true ) ),
'full_queue_size' => $full_queue->size(),
'full_queue_lag' => $full_queue->lag(),
'full_queue_next_sync' => ( self::$sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ),
)
);
}
}
// Check for WooCommerce support
add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'initialize_woocommerce' ), 5 );
// Check for WP Super Cache
add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'initialize_wp_super_cache' ), 5 );
/*
* Init after plugins loaded and before the `init` action. This helps with issues where plugins init
* with a high priority or sites that use alternate cron.
*/
add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'init' ), 90 );
// We need to define this here so that it's hooked before `updating_jetpack_version` is called
add_action( 'updating_jetpack_version', array( 'Jetpack_Sync_Actions', 'do_initial_sync' ), 10, 0 );
add_action( 'updating_jetpack_version', array( 'Jetpack_Sync_Actions', 'cleanup_on_upgrade' ), 10, 2 );
add_action( 'jetpack_user_authorized', array( 'Jetpack_Sync_Actions', 'do_initial_sync' ), 10, 0 );

View File

@@ -0,0 +1,572 @@
<?php
require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php' );
/**
* Just some defaults that we share with the server
*/
class Jetpack_Sync_Defaults {
static $default_options_whitelist = array(
'stylesheet',
'blogname',
'blogdescription',
'blog_charset',
'permalink_structure',
'category_base',
'tag_base',
'sidebars_widgets',
'comment_moderation',
'default_comment_status',
'page_on_front',
'rss_use_excerpt',
'subscription_options',
'stb_enabled',
'stc_enabled',
'comment_registration',
'show_avatars',
'avatar_default',
'avatar_rating',
'highlander_comment_form_prompt',
'jetpack_comment_form_color_scheme',
'stats_options',
'gmt_offset',
'timezone_string',
'jetpack_sync_non_public_post_stati',
'jetpack_options',
'site_icon', // (int) - ID of core's Site Icon attachment ID
'default_post_format',
'default_category',
'large_size_w',
'large_size_h',
'thumbnail_size_w',
'thumbnail_size_h',
'medium_size_w',
'medium_size_h',
'thumbnail_crop',
'image_default_link_type',
'site_logo',
'sharing-options',
'sharing-services',
'post_count',
'default_ping_status',
'sticky_posts',
'blog_public',
'default_pingback_flag',
'require_name_email',
'close_comments_for_old_posts',
'close_comments_days_old',
'thread_comments',
'thread_comments_depth',
'page_comments',
'comments_per_page',
'default_comments_page',
'comment_order',
'comments_notify',
'moderation_notify',
'social_notifications_like',
'social_notifications_reblog',
'social_notifications_subscribe',
'comment_whitelist',
'comment_max_links',
'moderation_keys',
'jetpack_wga',
'disabled_likes',
'disabled_reblogs',
'jetpack_comment_likes_enabled',
'twitter_via',
'jetpack-twitter-cards-site-tag',
'wpcom_publish_posts_with_markdown',
'wpcom_publish_comments_with_markdown',
'jetpack_activated',
'jetpack_available_modules',
'jetpack_autoupdate_plugins',
'jetpack_autoupdate_plugins_translations',
'jetpack_autoupdate_themes',
'jetpack_autoupdate_themes_translations',
'jetpack_autoupdate_core',
'jetpack_autoupdate_translations',
'carousel_background_color',
'carousel_display_exif',
'jetpack_portfolio',
'jetpack_portfolio_posts_per_page',
'jetpack_testimonial',
'jetpack_testimonial_posts_per_page',
'tiled_galleries',
'gravatar_disable_hovercards',
'infinite_scroll',
'infinite_scroll_google_analytics',
'wp_mobile_excerpt',
'wp_mobile_featured_images',
'wp_mobile_app_promos',
'monitor_receive_notifications',
'post_by_email_address',
'jetpack_protect_key',
'jetpack_protect_global_whitelist',
'jetpack_sso_require_two_step',
'jetpack_relatedposts',
'verification_services_codes',
'users_can_register',
'active_plugins',
'uninstall_plugins',
'advanced_seo_front_page_description', // Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION
'advanced_seo_title_formats', // Jetpack_SEO_Titles::TITLE_FORMATS_OPTION
'jetpack_api_cache_enabled',
'start_of_week',
'blacklist_keys',
'posts_per_page',
'posts_per_rss',
'show_on_front',
'ping_sites',
'uploads_use_yearmonth_folders',
'date_format',
'time_format',
'admin_email',
'new_admin_email',
'default_email_category',
'default_role',
'page_for_posts',
'mailserver_url',
'mailserver_login', // Not syncing contents, only the option name
'mailserver_pass', // Not syncing contents, only the option name
'mailserver_port',
'wp_page_for_privacy_policy',
);
public static function get_options_whitelist() {
/** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */
$options_whitelist = apply_filters( 'jetpack_options_whitelist', self::$default_options_whitelist );
/**
* Filter the list of WordPress options that are manageable via the JSON API.
*
* @module sync
*
* @since 4.8
*
* @param array The default list of options.
*/
return apply_filters( 'jetpack_sync_options_whitelist', $options_whitelist );
}
// Do not sync contents for these events, only the option name
static $default_options_contentless = array(
'mailserver_login',
'mailserver_pass',
);
public static function get_options_contentless() {
/**
* Filter the list of WordPress options that should be synced without content
*
* @module sync
*
* @since 6.1
*
* @param array The list of options synced without content.
*/
return apply_filters( 'jetpack_sync_options_contentless', self::$default_options_contentless );
}
static $default_constants_whitelist = array(
'EMPTY_TRASH_DAYS',
'WP_POST_REVISIONS',
'AUTOMATIC_UPDATER_DISABLED',
'ABSPATH',
'WP_CONTENT_DIR',
'FS_METHOD',
'DISALLOW_FILE_EDIT',
'DISALLOW_FILE_MODS',
'WP_AUTO_UPDATE_CORE',
'WP_HTTP_BLOCK_EXTERNAL',
'WP_ACCESSIBLE_HOSTS',
'JETPACK__VERSION',
'IS_PRESSABLE',
'DISABLE_WP_CRON',
'ALTERNATE_WP_CRON',
'WP_CRON_LOCK_TIMEOUT',
'PHP_VERSION',
'WP_MEMORY_LIMIT',
'WP_MAX_MEMORY_LIMIT'
);
public static function get_constants_whitelist() {
/**
* Filter the list of PHP constants that are manageable via the JSON API.
*
* @module sync
*
* @since 4.8
*
* @param array The default list of constants options.
*/
return apply_filters( 'jetpack_sync_constants_whitelist', self::$default_constants_whitelist );
}
static $default_callable_whitelist = array(
'wp_max_upload_size' => 'wp_max_upload_size',
'is_main_network' => array( 'Jetpack', 'is_multi_network' ),
'is_multi_site' => 'is_multisite',
'main_network_site' => array( 'Jetpack_Sync_Functions', 'main_network_site_url' ),
'site_url' => array( 'Jetpack_Sync_Functions', 'site_url' ),
'home_url' => array( 'Jetpack_Sync_Functions', 'home_url' ),
'single_user_site' => array( 'Jetpack', 'is_single_user_site' ),
'updates' => array( 'Jetpack', 'get_updates' ),
'has_file_system_write_access' => array( 'Jetpack_Sync_Functions', 'file_system_write_access' ),
'is_version_controlled' => array( 'Jetpack_Sync_Functions', 'is_version_controlled' ),
'taxonomies' => array( 'Jetpack_Sync_Functions', 'get_taxonomies' ),
'post_types' => array( 'Jetpack_Sync_Functions', 'get_post_types' ),
'post_type_features' => array( 'Jetpack_Sync_Functions', 'get_post_type_features' ),
'shortcodes' => array( 'Jetpack_Sync_Functions', 'get_shortcodes' ),
'rest_api_allowed_post_types' => array( 'Jetpack_Sync_Functions', 'rest_api_allowed_post_types' ),
'rest_api_allowed_public_metadata' => array( 'Jetpack_Sync_Functions', 'rest_api_allowed_public_metadata' ),
'sso_is_two_step_required' => array( 'Jetpack_SSO_Helpers', 'is_two_step_required' ),
'sso_should_hide_login_form' => array( 'Jetpack_SSO_Helpers', 'should_hide_login_form' ),
'sso_match_by_email' => array( 'Jetpack_SSO_Helpers', 'match_by_email' ),
'sso_new_user_override' => array( 'Jetpack_SSO_Helpers', 'new_user_override' ),
'sso_bypass_default_login_form' => array( 'Jetpack_SSO_Helpers', 'bypass_login_forward_wpcom' ),
'wp_version' => array( 'Jetpack_Sync_Functions', 'wp_version' ),
'get_plugins' => array( 'Jetpack_Sync_Functions', 'get_plugins' ),
'get_plugins_action_links' => array( 'Jetpack_Sync_functions', 'get_plugins_action_links' ),
'active_modules' => array( 'Jetpack', 'get_active_modules' ),
'hosting_provider' => array( 'Jetpack_Sync_Functions', 'get_hosting_provider' ),
'locale' => 'get_locale',
'site_icon_url' => array( 'Jetpack_Sync_Functions', 'site_icon_url' ),
'roles' => array( 'Jetpack_Sync_Functions', 'roles' ),
'timezone' => array( 'Jetpack_Sync_Functions', 'get_timezone' ),
);
static $default_post_type_attributes = array(
'name' => '',
'label' => '',
'labels' => array(),
'description' => '',
'public' => false,
'hierarchical' => false,
'exclude_from_search' => true,
'publicly_queryable' => null,
'show_ui' => false,
'show_in_menu' => null,
'show_in_nav_menus' => null,
'show_in_admin_bar' => false,
'menu_position' => null,
'menu_icon' => null,
'supports' => array(),
'capability_type' => 'post',
'capabilities' => array(),
'cap' => array(),
'map_meta_cap' => true,
'taxonomies' => array(),
'has_archive' => false,
'rewrite' => true,
'query_var' => true,
'can_export' => true,
'delete_with_user' => null,
'show_in_rest' => false,
'rest_base' => false,
'_builtin' => false,
'_edit_link' => 'post.php?post=%d',
);
public static function get_callable_whitelist() {
/**
* Filter the list of callables that are manageable via the JSON API.
*
* @module sync
*
* @since 4.8
*
* @param array The default list of callables.
*/
return apply_filters( 'jetpack_sync_callable_whitelist', self::$default_callable_whitelist );
}
static $blacklisted_post_types = array(
'ai1ec_event',
'bwg_album',
'bwg_gallery',
'customize_changeset', // WP built-in post type for Customizer changesets
'dn_wp_yt_log',
'http',
'idx_page',
'jetpack_migration',
'postman_sent_mail',
'rssap-feed',
'rssmi_feed_item',
'secupress_log_action',
'sg_optimizer_jobs',
'snitch',
'wpephpcompat_jobs',
'wprss_feed_item',
'wp_automatic',
'jp_sitemap_master',
'jp_sitemap',
'jp_sitemap_index',
'jp_img_sitemap',
'jp_img_sitemap_index',
'jp_vid_sitemap',
'jp_vid_sitemap_index',
);
static $default_post_checksum_columns = array(
'ID',
'post_modified',
);
static $default_post_meta_checksum_columns = array(
'meta_id',
'meta_value'
);
static $default_comment_checksum_columns = array(
'comment_ID',
'comment_content',
);
static $default_comment_meta_checksum_columns = array(
'meta_id',
'meta_value'
);
static $default_option_checksum_columns = array(
'option_name',
'option_value',
);
static $default_multisite_callable_whitelist = array(
'network_name' => array( 'Jetpack', 'network_name' ),
'network_allow_new_registrations' => array( 'Jetpack', 'network_allow_new_registrations' ),
'network_add_new_users' => array( 'Jetpack', 'network_add_new_users' ),
'network_site_upload_space' => array( 'Jetpack', 'network_site_upload_space' ),
'network_upload_file_types' => array( 'Jetpack', 'network_upload_file_types' ),
'network_enable_administration_menus' => array( 'Jetpack', 'network_enable_administration_menus' ),
);
public static function get_multisite_callable_whitelist() {
/**
* Filter the list of multisite callables that are manageable via the JSON API.
*
* @module sync
*
* @since 4.8
*
* @param array The default list of multisite callables.
*/
return apply_filters( 'jetpack_sync_multisite_callable_whitelist', self::$default_multisite_callable_whitelist );
}
static $post_meta_whitelist = array(
'_feedback_akismet_values',
'_feedback_email',
'_feedback_extra_fields',
'_g_feedback_shortcode',
'_jetpack_post_thumbnail',
'_menu_item_classes',
'_menu_item_menu_item_parent',
'_menu_item_object',
'_menu_item_object_id',
'_menu_item_orphaned',
'_menu_item_type',
'_menu_item_xfn',
'_publicize_facebook_user',
'_publicize_twitter_user',
'_thumbnail_id',
'_wp_attached_file',
'_wp_attachment_backup_sizes',
'_wp_attachment_context',
'_wp_attachment_image_alt',
'_wp_attachment_is_custom_background',
'_wp_attachment_is_custom_header',
'_wp_attachment_metadata',
'_wp_page_template',
'_wp_trash_meta_comments_status',
'_wpas_mess',
'content_width',
'custom_css_add',
'custom_css_preprocessor',
'enclosure',
'imagedata',
'nova_price',
'publicize_results',
'sharing_disabled',
'switch_like_status',
'videopress_guid',
'vimeo_poster_image',
'advanced_seo_description', // Jetpack_SEO_Posts::DESCRIPTION_META_KEY
);
public static function get_post_meta_whitelist() {
/**
* Filter the list of post meta data that are manageable via the JSON API.
*
* @module sync
*
* @since 4.8
*
* @param array The default list of meta data keys.
*/
return apply_filters( 'jetpack_sync_post_meta_whitelist', self::$post_meta_whitelist );
}
static $comment_meta_whitelist = array(
'hc_avatar',
'hc_post_as',
'hc_wpcom_id_sig',
'hc_foreign_user_id'
);
public static function get_comment_meta_whitelist() {
/**
* Filter the list of comment meta data that are manageable via the JSON API.
*
* @module sync
*
* @since 5.7.0
*
* @param array The default list of comment meta data keys.
*/
return apply_filters( 'jetpack_sync_comment_meta_whitelist', self::$comment_meta_whitelist );
}
// TODO: move this to server? - these are theme support values
// that should be synced as jetpack_current_theme_supports_foo option values
static $default_theme_support_whitelist = array(
'post-thumbnails',
'post-formats',
'custom-header',
'custom-background',
'custom-logo',
'menus',
'automatic-feed-links',
'editor-style',
'widgets',
'html5',
'title-tag',
'jetpack-social-menu',
'jetpack-responsive-videos',
'infinite-scroll',
'site-logo',
);
static function is_whitelisted_option( $option ) {
foreach ( self::$default_options_whitelist as $whitelisted_option ) {
if ( $whitelisted_option[0] === '/' && preg_match( $whitelisted_option, $option ) ) {
return true;
} elseif ( $whitelisted_option === $option ) {
return true;
}
}
return false;
}
static $default_capabilities_whitelist = array(
'switch_themes',
'edit_themes',
'edit_theme_options',
'install_themes',
'activate_plugins',
'edit_plugins',
'install_plugins',
'edit_users',
'edit_files',
'manage_options',
'moderate_comments',
'manage_categories',
'manage_links',
'upload_files',
'import',
'unfiltered_html',
'edit_posts',
'edit_others_posts',
'edit_published_posts',
'publish_posts',
'edit_pages',
'read',
'publish_pages',
'edit_others_pages',
'edit_published_pages',
'delete_pages',
'delete_others_pages',
'delete_published_pages',
'delete_posts',
'delete_others_posts',
'delete_published_posts',
'delete_private_posts',
'edit_private_posts',
'read_private_posts',
'delete_private_pages',
'edit_private_pages',
'read_private_pages',
'delete_users',
'create_users',
'unfiltered_upload',
'edit_dashboard',
'customize',
'delete_site',
'update_plugins',
'delete_plugins',
'update_themes',
'update_core',
'list_users',
'remove_users',
'add_users',
'promote_users',
'delete_themes',
'export',
'edit_comment',
'upload_plugins',
'upload_themes',
);
public static function get_capabilities_whitelist() {
/**
* Filter the list of capabilities that we care about
*
* @module sync
*
* @since 5.5.0
*
* @param array The default list of capabilities.
*/
return apply_filters( 'jetpack_sync_capabilities_whitelist', self::$default_capabilities_whitelist );
}
static function get_max_sync_execution_time() {
$max_exec_time = intval( ini_get( 'max_execution_time' ) );
if ( 0 === $max_exec_time ) {
// 0 actually means "unlimited", but let's not treat it that way
$max_exec_time = 60;
}
return floor( $max_exec_time / 3 );
}
static $default_network_options_whitelist = array(
'site_name',
'jetpack_protect_key',
'jetpack_protect_global_whitelist',
'active_sitewide_plugins',
);
static $default_taxonomy_whitelist = array();
static $default_dequeue_max_bytes = 500000; // very conservative value, 1/2 MB
static $default_upload_max_bytes = 600000; // a little bigger than the upload limit to account for serialization
static $default_upload_max_rows = 500;
static $default_sync_wait_time = 10; // seconds, between syncs
static $default_sync_wait_threshold = 5; // only wait before next send if the current send took more than X seconds
static $default_enqueue_wait_time = 10; // wait between attempting to continue a full sync, via requests
static $default_max_queue_size = 1000;
static $default_max_queue_lag = 900; // 15 minutes
static $default_queue_max_writes_sec = 100; // 100 rows a second
static $default_post_types_blacklist = array();
static $default_post_meta_whitelist = array();
static $default_comment_meta_whitelist = array();
static $default_disable = 0; // completely disable sending data to wpcom
static $default_sync_via_cron = 1; // use cron to sync
static $default_render_filtered_content = 0; // render post_filtered_content
static $default_max_enqueue_full_sync = 100; // max number of items to enqueue at a time when running full sync
static $default_max_queue_size_full_sync = 1000; // max number of total items in the full sync queue
static $default_sync_callables_wait_time = MINUTE_IN_SECONDS; // seconds before sending callables again
static $default_sync_constants_wait_time = HOUR_IN_SECONDS; // seconds before sending constants again
static $default_sync_queue_lock_timeout = 120; // 2 minutes
static $default_cron_sync_time_limit = 30; // 30 seconds
}

View File

@@ -0,0 +1,387 @@
<?php
/*
* Utility functions to generate data synced to wpcom
*/
class Jetpack_Sync_Functions {
const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_';
const HTTPS_CHECK_HISTORY = 5;
public static function get_modules() {
require_once( JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php' );
return Jetpack_Admin::init()->get_modules();
}
public static function get_taxonomies() {
global $wp_taxonomies;
$wp_taxonomies_without_callbacks = array();
foreach ( $wp_taxonomies as $taxonomy_name => $taxonomy ) {
$sanitized_taxonomy = self::sanitize_taxonomy( $taxonomy );
if ( ! empty( $sanitized_taxonomy ) ) {
$wp_taxonomies_without_callbacks[ $taxonomy_name ] = $sanitized_taxonomy;
} else {
error_log( 'Jetpack: Encountered a recusive taxonomy:' . $taxonomy_name );
}
}
return $wp_taxonomies_without_callbacks;
}
public static function get_shortcodes() {
global $shortcode_tags;
return array_keys( $shortcode_tags );
}
/**
* Removes any callback data since we will not be able to process it on our side anyways.
*/
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 ) );
// recursive taxonomies are no fun.
if ( is_null( $cloned_taxonomy ) ) {
return null;
}
// Remove any meta_box_cb if they are not the default wp ones.
if ( isset( $cloned_taxonomy->meta_box_cb ) &&
! in_array( $cloned_taxonomy->meta_box_cb, array( 'post_tags_meta_box', 'post_categories_meta_box' ) ) ) {
$cloned_taxonomy->meta_box_cb = null;
}
// Remove update call back
if ( isset( $cloned_taxonomy->update_count_callback ) &&
! is_null( $cloned_taxonomy->update_count_callback ) ) {
$cloned_taxonomy->update_count_callback = null;
}
// Remove rest_controller_class if it something other then the default.
if ( isset( $cloned_taxonomy->rest_controller_class ) &&
'WP_REST_Terms_Controller' !== $cloned_taxonomy->rest_controller_class ) {
$cloned_taxonomy->rest_controller_class = null;
}
return $cloned_taxonomy;
}
public static function get_post_types() {
global $wp_post_types;
$post_types_without_callbacks = array();
foreach ( $wp_post_types as $post_type_name => $post_type ) {
$sanitized_post_type = self::sanitize_post_type( $post_type );
if ( ! empty( $sanitized_post_type ) ) {
$post_types_without_callbacks[ $post_type_name ] = $sanitized_post_type;
} else {
error_log( 'Jetpack: Encountered a recusive post_type:' . $post_type_name );
}
}
return $post_types_without_callbacks;
}
public static function sanitize_post_type( $post_type ) {
// Lets clone the post type object instead of modifing the global one.
$sanitized_post_type = array();
foreach ( Jetpack_Sync_Defaults::$default_post_type_attributes as $attribute_key => $default_value ) {
if ( isset( $post_type->{ $attribute_key } ) ) {
$sanitized_post_type[ $attribute_key ] = $post_type->{ $attribute_key };
}
}
return (object) $sanitized_post_type;
}
public static function expand_synced_post_type( $sanitized_post_type, $post_type ) {
$post_type = sanitize_key( $post_type );
$post_type_object = new WP_Post_Type( $post_type, $sanitized_post_type );
$post_type_object->add_supports();
$post_type_object->add_rewrite_rules();
$post_type_object->add_hooks();
$post_type_object->register_taxonomies();
return (object) $post_type_object;
}
public static function get_post_type_features() {
global $_wp_post_type_features;
return $_wp_post_type_features;
}
public static function get_hosting_provider() {
if ( defined( 'GD_SYSTEM_PLUGIN_DIR' ) || class_exists( '\\WPaaS\\Plugin' ) ) {
return 'gd-managed-wp';
}
if ( defined( 'MM_BASE_DIR' ) ) {
return 'bh';
}
if ( defined( 'IS_PRESSABLE' ) ) {
return 'pressable';
}
if ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ) {
return 'wpe';
}
if ( defined( 'VIP_GO_ENV' ) && false !== VIP_GO_ENV ) {
return 'vip-go';
}
return 'unknown';
}
public static function rest_api_allowed_post_types() {
/** This filter is already documented in class.json-api-endpoints.php */
return apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
}
public static function rest_api_allowed_public_metadata() {
/** This filter is documented in json-endpoints/class.wpcom-json-api-post-endpoint.php */
return apply_filters( 'rest_api_allowed_public_metadata', array() );
}
/**
* Finds out if a site is using a version control system.
* @return bool
**/
public static function is_version_controlled() {
if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
require_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
}
$updater = new WP_Automatic_Updater();
return (bool) strval( $updater->is_vcs_checkout( $context = ABSPATH ) );
}
/**
* Returns true if the site has file write access false otherwise.
* @return bool
**/
public static function file_system_write_access() {
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );
}
require_once( ABSPATH . 'wp-admin/includes/template.php' );
$filesystem_method = get_filesystem_method();
if ( 'direct' === $filesystem_method ) {
return true;
}
ob_start();
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );
}
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
if ( $filesystem_credentials_are_stored ) {
return true;
}
return false;
}
/**
* Helper function that is used when getting home or siteurl values. Decides
* whether to get the raw or filtered value.
*
* @return string
*/
public static function get_raw_or_filtered_url( $url_type ) {
$url_function = ( 'home' == $url_type )
? 'home_url'
: 'site_url';
if (
! Jetpack_Constants::is_defined( 'JETPACK_SYNC_USE_RAW_URL' ) ||
Jetpack_Constants::get_constant( 'JETPACK_SYNC_USE_RAW_URL' )
) {
$scheme = is_ssl() ? 'https' : 'http';
$url = self::get_raw_url( $url_type );
$url = set_url_scheme( $url, $scheme );
} else {
$url = self::normalize_www_in_url( $url_type, $url_function );
}
return self::get_protocol_normalized_url( $url_function, $url );
}
public static function home_url() {
$url = self::get_raw_or_filtered_url( 'home' );
/**
* Allows overriding of the home_url value that is synced back to WordPress.com.
*
* @since 5.2
*
* @param string $home_url
*/
return esc_url_raw( apply_filters( 'jetpack_sync_home_url', $url ) );
}
public static function site_url() {
$url = self::get_raw_or_filtered_url( 'siteurl' );
/**
* Allows overriding of the site_url value that is synced back to WordPress.com.
*
* @since 5.2
*
* @param string $site_url
*/
return esc_url_raw( apply_filters( 'jetpack_sync_site_url', $url ) );
}
public static function main_network_site_url() {
return self::get_protocol_normalized_url( 'main_network_site_url', network_site_url() );
}
public static function get_protocol_normalized_url( $callable, $new_value ) {
$option_key = self::HTTPS_CHECK_OPTION_PREFIX . $callable;
$parsed_url = wp_parse_url( $new_value );
if ( ! $parsed_url ) {
return $new_value;
}
if ( array_key_exists ( 'scheme' , $parsed_url ) ) {
$scheme = $parsed_url['scheme'];
} else {
$scheme = '';
}
$scheme_history = get_option( $option_key, array() );
$scheme_history[] = $scheme;
// Limit length to self::HTTPS_CHECK_HISTORY
$scheme_history = array_slice( $scheme_history, ( self::HTTPS_CHECK_HISTORY * -1 ) );
update_option( $option_key, $scheme_history );
$forced_scheme = in_array( 'https', $scheme_history ) ? 'https' : 'http';
return set_url_scheme( $new_value, $forced_scheme );
}
public static function get_raw_url( $option_name ) {
$value = null;
$constant = ( 'home' == $option_name )
? 'WP_HOME'
: 'WP_SITEURL';
// Since we disregard the constant for multisites in ms-default-filters.php,
// let's also use the db value if this is a multisite.
if ( ! is_multisite() && Jetpack_Constants::is_defined( $constant ) ) {
$value = Jetpack_Constants::get_constant( $constant );
} else {
// Let's get the option from the database so that we can bypass filters. This will help
// ensure that we get more uniform values.
$value = Jetpack_Options::get_raw_option( $option_name );
}
return $value;
}
public static function normalize_www_in_url( $option, $url_function ) {
$url = wp_parse_url( call_user_func( $url_function ) );
$option_url = wp_parse_url( get_option( $option ) );
if ( ! $option_url || ! $url ) {
return $url;
}
if ( $url[ 'host' ] === "www.{$option_url[ 'host' ]}" ) {
// remove www if not present in option URL
$url[ 'host' ] = $option_url[ 'host' ];
}
if ( $option_url[ 'host' ] === "www.{$url[ 'host' ]}" ) {
// add www if present in option URL
$url[ 'host' ] = $option_url[ 'host' ];
}
$normalized_url = "{$url['scheme']}://{$url['host']}";
if ( isset( $url['path'] ) ) {
$normalized_url .= "{$url['path']}";
}
if ( isset( $url['query'] ) ) {
$normalized_url .= "?{$url['query']}";
}
return $normalized_url;
}
public static function get_plugins() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
/** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
return apply_filters( 'all_plugins', get_plugins() );
}
/**
* Get custom action link tags that the plugin is using
* Ref: https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
* @return array of plugin action links (key: link name value: url)
*/
public static function get_plugins_action_links( $plugin_file_singular = null ) {
// Some sites may have DOM disabled in PHP fail early
if ( ! class_exists( 'DOMDocument' ) ) {
return array();
}
$plugins_action_links = get_option( 'jetpack_plugin_api_action_links', array() );
if ( ! empty( $plugins_action_links ) ) {
if ( is_null( $plugin_file_singular ) ) {
return $plugins_action_links;
}
return ( isset( $plugins_action_links[ $plugin_file_singular ] ) ? $plugins_action_links[ $plugin_file_singular ] : null );
}
return array();
}
public static function wp_version() {
global $wp_version;
return $wp_version;
}
public static function site_icon_url( $size = 512 ) {
if ( ! function_exists( 'get_site_icon_url' ) || ! has_site_icon() ) {
return get_option( 'jetpack_site_icon_url' );
}
return get_site_icon_url( $size );
}
public static function roles() {
$wp_roles = wp_roles();
return $wp_roles->roles;
}
/**
* Determine time zone from WordPress' options "timezone_string"
* and "gmt_offset".
*
* 1. Check if `timezone_string` is set and return it.
* 2. Check if `gmt_offset` is set, formats UTC-offset from it and return it.
* 3. Default to "UTC+0" if nothing is set.
*
* @return string
*/
public static function get_timezone() {
$timezone_string = get_option( 'timezone_string' );
if ( ! empty( $timezone_string ) ) {
return str_replace( '_', ' ', $timezone_string );
}
$gmt_offset = get_option( 'gmt_offset', 0 );
$formatted_gmt_offset = sprintf( '%+g', floatval( $gmt_offset ) );
$formatted_gmt_offset = str_replace(
array( '.25', '.5', '.75' ),
array( ':15', ':30', ':45' ),
(string) $formatted_gmt_offset
);
/* translators: %s is UTC offset, e.g. "+1" */
return sprintf( __( 'UTC%s' ), $formatted_gmt_offset );
}
}

View File

@@ -0,0 +1,85 @@
<?php
require_once dirname( __FILE__ ) . '/interface.jetpack-sync-codec.php';
/**
* An implementation of iJetpack_Sync_Codec that uses gzip's DEFLATE
* algorithm to compress objects serialized using json_encode
*/
class Jetpack_Sync_JSON_Deflate_Array_Codec implements iJetpack_Sync_Codec {
const CODEC_NAME = "deflate-json-array";
public function name() {
return self::CODEC_NAME;
}
public function encode( $object ) {
return base64_encode( gzdeflate( $this->json_serialize( $object ) ) );
}
public function decode( $input ) {
return $this->json_unserialize( gzinflate( base64_decode( $input ) ) );
}
// @see https://gist.github.com/muhqu/820694
protected function json_serialize( $any ) {
if ( function_exists( 'jetpack_json_wrap' ) ) {
return wp_json_encode( jetpack_json_wrap( $any ) );
}
// This prevents fatal error when updating pre 6.0 via the cli command
return wp_json_encode( $this->json_wrap( $any ) );
}
protected function json_unserialize( $str ) {
return $this->json_unwrap( json_decode( $str, true ) );
}
private function json_wrap( &$any, $seen_nodes = array() ) {
if ( is_object( $any ) ) {
$input = get_object_vars( $any );
$input['__o'] = 1;
} else {
$input = &$any;
}
if ( is_array( $input ) ) {
$seen_nodes[] = &$any;
$return = array();
foreach ( $input as $k => &$v ) {
if ( ( is_array( $v ) || is_object( $v ) ) ) {
if ( in_array( $v, $seen_nodes, true ) ) {
continue;
}
$return[ $k ] = $this->json_wrap( $v, $seen_nodes );
} else {
$return[ $k ] = $v;
}
}
return $return;
}
return $any;
}
private function json_unwrap( $any ) {
if ( is_array( $any ) ) {
foreach ( $any as $k => $v ) {
if ( '__o' === $k ) {
continue;
}
$any[ $k ] = $this->json_unwrap( $v );
}
if ( isset( $any['__o'] ) ) {
unset( $any['__o'] );
$any = (object) $any;
}
}
return $any;
}
}

View File

@@ -0,0 +1,303 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-queue.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-actions.php';
/**
* This class monitors actions and logs them to the queue to be sent
*/
class Jetpack_Sync_Listener {
const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state';
const QUEUE_STATE_CHECK_TIMEOUT = 300; // 5 minutes
private $sync_queue;
private $full_sync_queue;
private $sync_queue_size_limit;
private $sync_queue_lag_limit;
// singleton functions
private static $instance;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
// this is necessary because you can't use "new" when you declare instance properties >:(
protected function __construct() {
$this->set_defaults();
$this->init();
}
private function init() {
$handler = array( $this, 'action_handler' );
$full_sync_handler = array( $this, 'full_sync_action_handler' );
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module->init_listeners( $handler );
$module->init_full_sync_listeners( $full_sync_handler );
}
// Module Activation
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 );
}
function get_sync_queue() {
return $this->sync_queue;
}
function get_full_sync_queue() {
return $this->full_sync_queue;
}
function set_queue_size_limit( $limit ) {
$this->sync_queue_size_limit = $limit;
}
function get_queue_size_limit() {
return $this->sync_queue_size_limit;
}
function set_queue_lag_limit( $age ) {
$this->sync_queue_lag_limit = $age;
}
function get_queue_lag_limit() {
return $this->sync_queue_lag_limit;
}
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 );
}
// prevent adding items to the queue if it hasn't sent an item for 15 mins
// AND the queue is over 1000 items long (by default)
function can_add_to_queue( $queue ) {
if ( Jetpack_Sync_Settings::get_setting( 'disable' ) ) {
return false;
}
$state_transient_name = self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $queue->id;
$queue_state = get_transient( $state_transient_name );
if ( false === $queue_state ) {
$queue_state = array( $queue->size(), $queue->lag() );
set_transient( $state_transient_name, $queue_state, self::QUEUE_STATE_CHECK_TIMEOUT );
}
list( $queue_size, $queue_age ) = $queue_state;
return ( $queue_age < $this->sync_queue_lag_limit )
||
( ( $queue_size + 1 ) < $this->sync_queue_size_limit );
}
function full_sync_action_handler() {
$args = func_get_args();
$this->enqueue_action( current_filter(), $args, $this->full_sync_queue );
}
function action_handler() {
$args = func_get_args();
$this->enqueue_action( current_filter(), $args, $this->sync_queue );
}
// add many actions to the queue directly, without invoking them
function bulk_enqueue_full_sync_actions( $action_name, $args_array ) {
$queue = $this->get_full_sync_queue();
// periodically check the size of the queue, and disable adding to it if
// it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped)
if ( ! $this->can_add_to_queue( $queue ) ) {
return;
}
// if we add any items to the queue, we should try to ensure that our script
// can't be killed before they are sent
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
$data_to_enqueue = array();
$user_id = get_current_user_id();
$currtime = microtime( true );
$is_importing = Jetpack_Sync_Settings::is_importing();
foreach( $args_array as $args ) {
/**
* Modify or reject the data within an action before it is enqueued locally.
*
* @since 4.2.0
*
* @module sync
*
* @param array The action parameters
*/
$args = apply_filters( "jetpack_sync_before_enqueue_$action_name", $args );
// allow listeners to abort
if ( $args === false ) {
continue;
}
$data_to_enqueue[] = array(
$action_name,
array( $args ),
$user_id,
$currtime,
$is_importing,
);
}
$queue->add_all( $data_to_enqueue );
}
function enqueue_action( $current_filter, $args, $queue ) {
// don't enqueue an action during the outbound http request - this prevents recursion
if ( Jetpack_Sync_Settings::is_sending() ) {
return;
}
/**
* Add an action hook to execute when anything on the whitelist gets sent to the queue to sync.
*
* @module sync
*
* @since 5.9.0
*/
do_action( 'jetpack_sync_action_before_enqueue' );
/**
* Modify or reject the data within an action before it is enqueued locally.
*
* @since 4.2.0
*
* @param array The action parameters
*/
$args = apply_filters( "jetpack_sync_before_enqueue_$current_filter", $args );
// allow listeners to abort
if ( $args === false ) {
return;
}
// periodically check the size of the queue, and disable adding to it if
// it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped)
if ( ! $this->can_add_to_queue( $queue ) ) {
return;
}
// if we add any items to the queue, we should try to ensure that our script
// can't be killed before they are sent
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
if (
'sync' === $queue->id ||
in_array(
$current_filter,
array(
'jetpack_full_sync_start',
'jetpack_full_sync_end',
'jetpack_full_sync_cancel'
)
)
) {
$queue->add( array(
$current_filter,
$args,
get_current_user_id(),
microtime( true ),
Jetpack_Sync_Settings::is_importing(),
$this->get_actor( $current_filter, $args ),
) );
} else {
$queue->add( array(
$current_filter,
$args,
get_current_user_id(),
microtime( true ),
Jetpack_Sync_Settings::is_importing()
) );
}
// since we've added some items, let's try to load the sender so we can send them as quickly as possible
if ( ! Jetpack_Sync_Actions::$sender ) {
add_filter( 'jetpack_sync_sender_should_load', '__return_true' );
if ( did_action( 'init' ) ) {
Jetpack_Sync_Actions::add_sender_shutdown();
}
}
}
function get_actor( $current_filter, $args ) {
if ( 'wp_login' === $current_filter ) {
$user = get_user_by( 'ID', $args[1]->data->ID );
} else {
$user = wp_get_current_user();
}
$translated_role = Jetpack::translate_user_to_role( $user );
$actor = array(
'wpcom_user_id' => null,
'external_user_id' => isset( $user->ID ) ? $user->ID : null,
'display_name' => isset( $user->display_name ) ? $user->display_name : null,
'user_email' => isset( $user->user_email ) ? $user->user_email : null,
'user_roles' => isset( $user->roles ) ? $user->roles : null,
'translated_role' => $translated_role ? $translated_role : null,
'is_cron' => defined( 'DOING_CRON' ) ? DOING_CRON : false,
'is_rest' => defined( 'REST_API_REQUEST' ) ? REST_API_REQUEST : false,
'is_xmlrpc' => defined( 'XMLRPC_REQUEST' ) ? XMLRPC_REQUEST : false,
'is_wp_rest' => defined( 'REST_REQUEST' ) ? REST_REQUEST : false,
'is_ajax' => defined( 'DOING_AJAX' ) ? DOING_AJAX : false,
'is_wp_admin' => is_admin(),
);
if ( $this->should_send_user_data_with_actor( $current_filter ) ) {
require_once( JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php' );
$actor['ip'] = jetpack_protect_get_ip();
$actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown';
}
return $actor;
}
function should_send_user_data_with_actor( $current_filter ) {
$should_send = in_array( $current_filter, array( 'wp_login', 'wp_logout', 'jetpack_valid_failed_login_attempt' ) );
/**
* Allow or deny sending actor's user data ( IP and UA ) during a sync event
*
* @since 5.8.0
*
* @module sync
*
* @param bool True if we should send user data
* @param string The current filter that is performing the sync action
*/
return apply_filters( 'jetpack_sync_actor_user_data', $should_send, $current_filter );
}
function set_defaults() {
$this->sync_queue = new Jetpack_Sync_Queue( 'sync' );
$this->full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' );
$this->set_queue_size_limit( Jetpack_Sync_Settings::get_setting( 'max_queue_size' ) );
$this->set_queue_lag_limit( Jetpack_Sync_Settings::get_setting( 'max_queue_lag' ) );
}
}

View File

@@ -0,0 +1,43 @@
<?php
class Jetpack_Sync_Module_Attachments extends Jetpack_Sync_Module {
function name() {
return 'attachments';
}
public function init_listeners( $callable ) {
add_action( 'edit_attachment', array( $this, 'send_attachment_info' ) );
// Once we don't have to support 4.3 we can start using add_action( 'attachment_updated', $handler, 10, 3 ); instead
add_action( 'add_attachment', array( $this, 'send_attachment_info' ) );
add_action( 'jetpack_sync_save_update_attachment', $callable, 10, 2 );
add_action( 'jetpack_sync_save_add_attachment', $callable, 10, 2 );
}
function send_attachment_info( $attachment_id ) {
$attachment = get_post( $attachment_id );
if ( 'add_attachment' === current_filter() ) {
/**
* Fires when the client needs to sync an new attachment
*
* @since 4.2.0
*
* @param int The attachment ID
* @param object The attachment
*/
do_action( 'jetpack_sync_save_add_attachment', $attachment_id, $attachment );
} else {
/**
* Fires when the client needs to sync an updated attachment
*
* @since 4.9.0
*
* @param int The attachment ID
* @param object The attachment
*
* Previously this action was synced using jetpack_sync_save_add_attachment action.
*/
do_action( 'jetpack_sync_save_update_attachment', $attachment_id, $attachment );
}
}
}

View File

@@ -0,0 +1,264 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-functions.php';
class Jetpack_Sync_Module_Callables extends Jetpack_Sync_Module {
const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum';
const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await';
private $callable_whitelist;
public function name() {
return 'functions';
}
public function set_defaults() {
if ( is_multisite() ) {
$this->callable_whitelist = array_merge( Jetpack_Sync_Defaults::get_callable_whitelist(), Jetpack_Sync_Defaults::get_multisite_callable_whitelist() );
} else {
$this->callable_whitelist = Jetpack_Sync_Defaults::get_callable_whitelist();
}
}
public function init_listeners( $callable ) {
add_action( 'jetpack_sync_callable', $callable, 10, 2 );
add_action( 'admin_init', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late
// For some options, we should always send the change right away!
$always_send_updates_to_these_options = array(
'jetpack_active_modules',
'home',
'siteurl',
'jetpack_sync_error_idc',
);
foreach( $always_send_updates_to_these_options as $option ) {
add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
}
// Provide a hook so that hosts can send changes to certain callables right away.
// Especially useful when a host uses constants to change home and siteurl.
add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
// get_plugins and wp_version
// gets fired when new code gets installed, updates etc.
add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_callables', $callable );
}
public function init_before_send() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
}
public function reset_data() {
delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
$url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
foreach( $url_callables as $callable ) {
delete_option( Jetpack_Sync_Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
}
}
function set_callable_whitelist( $callables ) {
$this->callable_whitelist = $callables;
}
function get_callable_whitelist() {
return $this->callable_whitelist;
}
public function get_all_callables() {
// get_all_callables should run as the master user always.
$current_user_id = get_current_user_id();
wp_set_current_user( Jetpack_Options::get_option( 'master_user' ) );
$callables = array_combine(
array_keys( $this->get_callable_whitelist() ),
array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
);
wp_set_current_user( $current_user_id );
return $callables;
}
private function get_callable( $callable ) {
return call_user_func( $callable );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
/**
* Tells the client to sync all callables to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand callables (should always be true)
*/
do_action( 'jetpack_full_sync_callables', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
public function estimate_full_sync_actions( $config ) {
return 1;
}
public function get_full_sync_actions() {
return array( 'jetpack_full_sync_callables' );
}
public function unlock_sync_callable() {
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
}
public function unlock_plugin_action_link_and_callables() {
delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
delete_transient( 'jetpack_plugin_api_action_links_refresh' );
add_filter( 'jetpack_check_and_send_callables', '__return_true' );
}
public function set_plugin_action_links() {
if (
! class_exists( 'DOMDocument' ) ||
! function_exists ( 'libxml_use_internal_errors' ) ||
! function_exists ( 'mb_convert_encoding' )
) {
return;
}
$plugins_action_links = array();
// Is the transient lock in place?
$plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false );
if ( ! empty( $plugins_lock ) ) {
return;
}
$plugins = array_keys( Jetpack_Sync_Functions::get_plugins() );
foreach ( $plugins as $plugin_file ) {
/**
* Plugins often like to unset things but things break if they are not able to.
*/
$action_links = array(
'deactivate' => '',
'activate' => '',
'details' => '',
'delete' => '',
'edit' => ''
);
/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
$action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' );
/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
$action_links = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' );
$action_links = array_filter( $action_links );
$formatted_action_links = null;
if ( ! empty( $action_links ) && count( $action_links ) > 0 ) {
$dom_doc = new DOMDocument;
foreach ( $action_links as $action_link ) {
// The @ is not enough to suppress errors when dealing with libxml,
// we have to tell it directly how we want to handle errors.
libxml_use_internal_errors( true );
$dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) );
libxml_use_internal_errors( false );
$link_elements = $dom_doc->getElementsByTagName( 'a' );
if ( $link_elements->length == 0 ) {
continue;
}
$link_element = $link_elements->item( 0 );
if ( $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) {
$link_url = trim( $link_element->getAttribute( 'href' ) );
// Add the full admin path to the url if the plugin did not provide it
$link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
if ( empty( $link_url_scheme ) ) {
$link_url = admin_url( $link_url );
}
$formatted_action_links[ $link_element->nodeValue ] = $link_url;
}
}
}
if ( $formatted_action_links ) {
$plugins_action_links[ $plugin_file ] = $formatted_action_links;
}
}
// Cache things for a long time
set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
}
public function should_send_callable( $callable_checksums, $name, $checksum ) {
$idc_override_callables = array(
'main_network_site',
'home_url',
'site_url',
);
if ( in_array( $name, $idc_override_callables ) && Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
return true;
}
return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
}
public function maybe_sync_callables() {
if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
if ( ! is_admin() || Jetpack_Sync_Settings::is_doing_cron() ) {
return;
}
if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
return;
}
}
set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Jetpack_Sync_Defaults::$default_sync_callables_wait_time );
$callables = $this->get_all_callables();
if ( empty( $callables ) ) {
return;
}
$callable_checksums = (array) Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
// only send the callables that have changed
foreach ( $callables as $name => $value ) {
$checksum = $this->get_check_sum( $value );
// explicitly not using Identical comparison as get_option returns a string
if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
/**
* Tells the client to sync a callable (aka function) to the server
*
* @since 4.2.0
*
* @param string The name of the callable
* @param mixed The value of the callable
*/
do_action( 'jetpack_sync_callable', $name, $value );
$callable_checksums[ $name ] = $checksum;
} else {
$callable_checksums[ $name ] = $checksum;
}
}
Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
}
public function expand_callables( $args ) {
if ( $args[0] ) {
$callables = $this->get_all_callables();
$callables_checksums = array();
foreach ( $callables as $name => $value ) {
$callables_checksums[ $name ] = $this->get_check_sum( $value );
}
Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
return $callables;
}
return $args;
}
}

View File

@@ -0,0 +1,188 @@
<?php
class Jetpack_Sync_Module_Comments extends Jetpack_Sync_Module {
public function name() {
return 'comments';
}
public function get_object_by_id( $object_type, $id ) {
$comment_id = intval( $id );
if ( $object_type === 'comment' && $comment = get_comment( $comment_id ) ) {
return $this->filter_comment( $comment );
}
return false;
}
public function init_listeners( $callable ) {
add_action( 'wp_insert_comment', $callable, 10, 2 );
add_action( 'deleted_comment', $callable );
add_action( 'trashed_comment', $callable );
add_action( 'spammed_comment', $callable );
add_action( 'trashed_post_comments', $callable, 10, 2 );
add_action( 'untrash_post_comments', $callable );
add_action( 'comment_approved_to_unapproved', $callable );
add_action( 'comment_unapproved_to_approved', $callable );
add_action( 'jetpack_modified_comment_contents', $callable, 10, 2 );
add_action( 'untrashed_comment', $callable, 10, 2 );
add_action( 'unspammed_comment', $callable, 10, 2 );
add_filter( 'wp_update_comment_data', array( $this, 'handle_comment_contents_modification' ), 10, 3 );
// even though it's messy, we implement these hooks because
// the edit_comment hook doesn't include the data
// so this saves us a DB read for every comment event
foreach ( array( '', 'trackback', 'pingback' ) as $comment_type ) {
foreach ( array( 'unapproved', 'approved' ) as $comment_status ) {
$comment_action_name = "comment_{$comment_status}_{$comment_type}";
add_action( $comment_action_name, $callable, 10, 2 );
}
}
// listen for meta changes
$this->init_listeners_for_meta_type( 'comment', $callable );
$this->init_meta_whitelist_handler( 'comment', array( $this, 'filter_meta' ) );
}
public function handle_comment_contents_modification( $new_comment, $old_comment, $new_comment_with_slashes ) {
$content_fields = array(
'comment_author',
'comment_author_email',
'comment_author_url',
'comment_content',
);
$changes = array();
foreach ( $content_fields as $field ) {
if ( $new_comment_with_slashes[$field] != $old_comment[$field] ) {
$changes[$field] = array( $new_comment[$field], $old_comment[$field] );
}
}
if ( ! empty( $changes ) ) {
/**
* Signals to the sync listener that this comment's contents were modified and a sync action
* reflecting the change(s) to the content should be sent
*
* @since 4.9.0
*
* @param int $new_comment['comment_ID'] ID of comment whose content was modified
* @param mixed $changes Array of changed comment fields with before and after values
*/
do_action( 'jetpack_modified_comment_contents', $new_comment['comment_ID'], $changes );
}
return $new_comment;
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_comments', $callable ); // also send comments meta
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_wp_insert_comment', array( $this, 'expand_wp_insert_comment' ) );
foreach ( array( '', 'trackback', 'pingback' ) as $comment_type ) {
foreach ( array( 'unapproved', 'approved' ) as $comment_status ) {
$comment_action_name = "comment_{$comment_status}_{$comment_type}";
add_filter( 'jetpack_sync_before_send_' . $comment_action_name, array(
$this,
'expand_wp_insert_comment',
) );
}
}
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $wpdb->comments";
if ( $where_sql = $this->get_where_sql( $config ) ) {
$query .= ' WHERE ' . $where_sql;
}
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
private function get_where_sql( $config ) {
if ( is_array( $config ) ) {
return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return null;
}
public function get_full_sync_actions() {
return array( 'jetpack_full_sync_comments' );
}
public function count_full_sync_actions( $action_names ) {
return $this->count_actions( $action_names, array( 'jetpack_full_sync_comments' ) );
}
function expand_wp_comment_status_change( $args ) {
return array( $args[0], $this->filter_comment( $args[1] ) );
}
function expand_wp_insert_comment( $args ) {
return array( $args[0], $this->filter_comment( $args[1] ) );
}
function filter_comment( $comment ) {
/**
* Filters whether to prevent sending comment data to .com
*
* Passing true to the filter will prevent the comment data from being sent
* to the WordPress.com.
* Instead we pass data that will still enable us to do a checksum against the
* Jetpacks data but will prevent us from displaying the data on in the API as well as
* other services.
* @since 4.2.0
*
* @param boolean false prevent post data from bing synced to WordPress.com
* @param mixed $comment WP_COMMENT object
*/
if ( apply_filters( 'jetpack_sync_prevent_sending_comment_data', false, $comment ) ) {
$blocked_comment = new stdClass();
$blocked_comment->comment_ID = $comment->comment_ID;
$blocked_comment->comment_date = $comment->comment_date;
$blocked_comment->comment_date_gmt = $comment->comment_date_gmt;
$blocked_comment->comment_approved = 'jetpack_sync_blocked';
return $blocked_comment;
}
return $comment;
}
// Comment Meta
function is_whitelisted_comment_meta( $meta_key ) {
return in_array( $meta_key, Jetpack_Sync_Settings::get_setting( 'comment_meta_whitelist' ) );
}
function filter_meta( $args ) {
return ( $this->is_whitelisted_comment_meta( $args[2] ) ? $args : false );
}
public function expand_comment_ids( $args ) {
$comment_ids = $args[0];
$comments = get_comments( array(
'include_unapproved' => true,
'comment__in' => $comment_ids,
) );
return array(
$comments,
$this->get_metadata( $comment_ids, 'comment', Jetpack_Sync_Settings::get_setting( 'comment_meta_whitelist' ) ),
);
}
}

View File

@@ -0,0 +1,125 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
class Jetpack_Sync_Module_Constants extends Jetpack_Sync_Module {
const CONSTANTS_CHECKSUM_OPTION_NAME = 'jetpack_constants_sync_checksum';
const CONSTANTS_AWAIT_TRANSIENT_NAME = 'jetpack_sync_constants_await';
public function name() {
return 'constants';
}
public function init_listeners( $callable ) {
add_action( 'jetpack_sync_constant', $callable, 10, 2 );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_constants', $callable );
}
public function init_before_send() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_constants' ) );
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_constants', array( $this, 'expand_constants' ) );
}
public function reset_data() {
delete_option( self::CONSTANTS_CHECKSUM_OPTION_NAME );
delete_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME );
}
function set_constants_whitelist( $constants ) {
$this->constants_whitelist = $constants;
}
function get_constants_whitelist() {
return Jetpack_Sync_Defaults::get_constants_whitelist();
}
function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
/**
* Tells the client to sync all constants to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand constants (should always be true)
*/
do_action( 'jetpack_full_sync_constants', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
function estimate_full_sync_actions( $config ) {
return 1;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_constants' );
}
function maybe_sync_constants() {
if ( get_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ) ) {
return;
}
set_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME, microtime( true ), Jetpack_Sync_Defaults::$default_sync_constants_wait_time );
$constants = $this->get_all_constants();
if ( empty( $constants ) ) {
return;
}
$constants_checksums = (array) get_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, array() );
foreach ( $constants as $name => $value ) {
$checksum = $this->get_check_sum( $value );
// explicitly not using Identical comparison as get_option returns a string
if ( ! $this->still_valid_checksum( $constants_checksums, $name, $checksum ) && ! is_null( $value ) ) {
/**
* Tells the client to sync a constant to the server
*
* @since 4.2.0
*
* @param string The name of the constant
* @param mixed The value of the constant
*/
do_action( 'jetpack_sync_constant', $name, $value );
$constants_checksums[ $name ] = $checksum;
} else {
$constants_checksums[ $name ] = $checksum;
}
}
update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums );
}
// public so that we don't have to store an option for each constant
function get_all_constants() {
$constants_whitelist = $this->get_constants_whitelist();
return array_combine(
$constants_whitelist,
array_map( array( $this, 'get_constant' ), $constants_whitelist )
);
}
private function get_constant( $constant ) {
return ( defined( $constant ) ) ?
constant( $constant )
: null;
}
public function expand_constants( $args ) {
if ( $args[0] ) {
$constants = $this->get_all_constants();
$constants_checksums = array();
foreach ( $constants as $name => $value ) {
$constants_checksums[ $name ] = $this->get_check_sum( $value );
}
update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums );
return $constants;
}
return $args;
}
}

View File

@@ -0,0 +1,366 @@
<?php
/**
* This class does a full resync of the database by
* enqueuing an outbound action for every single object
* that we care about.
*
* This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained:
* - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database
* - for each object type, we page through the object IDs and enqueue them by firing some monitored actions
* - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
* - we fire a trigger for the entire array which the Jetpack_Sync_Listener then serializes and queues.
*/
class Jetpack_Sync_Module_Full_Sync extends Jetpack_Sync_Module {
const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
const FULL_SYNC_TIMEOUT = 3600;
public function name() {
return 'full-sync';
}
function init_full_sync_listeners( $callable ) {
// synthetic actions for full sync
add_action( 'jetpack_full_sync_start', $callable );
add_action( 'jetpack_full_sync_end', $callable );
add_action( 'jetpack_full_sync_cancelled', $callable );
}
function init_before_send() {
// this is triggered after actions have been processed on the server
add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
}
function start( $module_configs = null ) {
$was_already_running = $this->is_started() && ! $this->is_finished();
// remove all evidence of previous full sync items and status
$this->reset_data();
if ( $was_already_running ) {
/**
* Fires when a full sync is cancelled.
*
* @since 4.2.0
*/
do_action( 'jetpack_full_sync_cancelled' );
}
$this->update_status_option( 'started', time() );
$this->update_status_option( 'params', $module_configs );
$enqueue_status = array();
$full_sync_config = array();
// default value is full sync
if ( ! is_array( $module_configs ) ) {
$module_configs = array();
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module_configs[ $module->name() ] = true;
}
}
// set default configuration, calculate totals, and save configuration if totals > 0
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module_name = $module->name();
$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
if ( ! $module_config ) {
continue;
}
if ( 'users' === $module_name && 'initial' === $module_config ) {
$module_config = $module->get_initial_sync_user_config();
}
$enqueue_status[ $module_name ] = false;
$total_items = $module->estimate_full_sync_actions( $module_config );
// if there's information to process, configure this module
if ( ! is_null( $total_items ) && $total_items > 0 ) {
$full_sync_config[ $module_name ] = $module_config;
$enqueue_status[ $module_name ] = array(
$total_items, // total
0, // queued
false, // current state
);
}
}
$this->set_config( $full_sync_config );
$this->set_enqueue_status( $enqueue_status );
/**
* Fires when a full sync begins. This action is serialized
* and sent to the server so that it knows a full sync is coming.
*
* @since 4.2.0
*/
do_action( 'jetpack_full_sync_start', $full_sync_config );
$this->continue_enqueuing( $full_sync_config, $enqueue_status );
return true;
}
function continue_enqueuing( $configs = null, $enqueue_status = null ) {
if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) {
return;
}
// if full sync queue is full, don't enqueue more items
$max_queue_size_full_sync = Jetpack_Sync_Settings::get_setting( 'max_queue_size_full_sync' );
$full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' );
$available_queue_slots = $max_queue_size_full_sync - $full_sync_queue->size();
if ( $available_queue_slots <= 0 ) {
return;
} else {
$remaining_items_to_enqueue = min( Jetpack_Sync_Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots );
}
if ( ! $configs ) {
$configs = $this->get_config();
}
if ( ! $enqueue_status ) {
$enqueue_status = $this->get_enqueue_status();
}
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module_name = $module->name();
// skip module if not configured for this sync or module is done
if ( ! isset( $configs[ $module_name ] )
|| // no module config
! $configs[ $module_name ]
|| // no enqueue status
! $enqueue_status[ $module_name ]
|| // finished enqueuing this module
true === $enqueue_status[ $module_name ][ 2 ] ) {
continue;
}
list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][ 2 ] );
$enqueue_status[ $module_name ][ 2 ] = $next_enqueue_state;
// if items were processed, subtract them from the limit
if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
$enqueue_status[ $module_name ][ 1 ] += $items_enqueued;
$remaining_items_to_enqueue -= $items_enqueued;
}
// stop processing if we've reached our limit of items to enqueue
if ( 0 >= $remaining_items_to_enqueue ) {
$this->set_enqueue_status( $enqueue_status );
return;
}
}
$this->set_enqueue_status( $enqueue_status );
// setting autoload to true means that it's faster to check whether we should continue enqueuing
$this->update_status_option( 'queue_finished', time(), true );
/**
* Fires when a full sync ends. This action is serialized
* and sent to the server.
*
* @since 4.2.0
*/
do_action( 'jetpack_full_sync_end', '' );
}
function update_sent_progress_action( $actions ) {
// quick way to map to first items with an array of arrays
$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
if ( ! $this->is_started() || $this->is_finished() ) {
return;
}
if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
$this->update_status_option( 'send_started', time() );
}
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module_actions = $module->get_full_sync_actions();
$status_option_name = "{$module->name()}_sent";
$items_sent = $this->get_status_option( $status_option_name, 0 );
foreach ( $module_actions as $module_action ) {
if ( isset( $actions_with_counts[ $module_action ] ) ) {
$items_sent += $actions_with_counts[ $module_action ];
}
}
if ( $items_sent > 0 ) {
$this->update_status_option( $status_option_name, $items_sent );
}
}
if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
$this->update_status_option( 'finished', time() );
}
}
public function get_action_name( $queue_item ) {
if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
return $queue_item[0];
}
return false;
}
public function is_started() {
return !! $this->get_status_option( 'started' );
}
public function is_finished() {
return !! $this->get_status_option( 'finished' );
}
public function get_status() {
$status = array(
'started' => $this->get_status_option( 'started' ),
'queue_finished' => $this->get_status_option( 'queue_finished' ),
'send_started' => $this->get_status_option( 'send_started' ),
'finished' => $this->get_status_option( 'finished' ),
'sent' => array(),
'queue' => array(),
'config' => $this->get_status_option( 'params' ),
'total' => array(),
);
$enqueue_status = $this->get_enqueue_status();
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$name = $module->name();
if ( ! isset( $enqueue_status[ $name ] ) ) {
continue;
}
list( $total, $queued, $state ) = $enqueue_status[ $name ];
if ( $total ) {
$status[ 'total' ][ $name ] = $total;
}
if ( $queued ) {
$status[ 'queue' ][ $name ] = $queued;
}
if ( $sent = $this->get_status_option( "{$name}_sent" ) ) {
$status[ 'sent' ][ $name ] = $sent;
}
}
return $status;
}
public function clear_status() {
$prefix = self::STATUS_OPTION_PREFIX;
Jetpack_Options::delete_raw_option( "{$prefix}_started" );
Jetpack_Options::delete_raw_option( "{$prefix}_params" );
Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
$this->delete_enqueue_status();
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
}
}
public function reset_data() {
$this->clear_status();
$this->delete_config();
require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php';
$listener = Jetpack_Sync_Listener::get_instance();
$listener->get_full_sync_queue()->reset();
}
private function get_status_option( $name, $default = null ) {
$value = Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
return is_numeric( $value ) ? intval( $value ) : $value;
}
private function update_status_option( $name, $value, $autoload = false ) {
Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
}
private function set_enqueue_status( $new_status ) {
Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
}
private function delete_enqueue_status() {
return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
}
private function get_enqueue_status() {
return Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
}
private function set_config( $config ) {
Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
}
private function delete_config() {
return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
}
private function get_config() {
return Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
}
private function write_option( $name, $value ) {
// we write our own option updating code to bypass filters/caching/etc on set_option/get_option
global $wpdb;
$serialized_value = maybe_serialize( $value );
// try updating, if no update then insert
// TODO: try to deal with the fact that unchanged values can return updated_num = 0
// below we used "insert ignore" to at least suppress the resulting error
$updated_num = $wpdb->query(
$wpdb->prepare(
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
$serialized_value,
$name
)
);
if ( ! $updated_num ) {
$updated_num = $wpdb->query(
$wpdb->prepare(
"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
$name,
$serialized_value
)
);
}
return $updated_num;
}
private function read_option( $name, $default = null ) {
global $wpdb;
$value = $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
$name
)
);
$value = maybe_unserialize( $value );
if ( $value === null && $default !== null ) {
return $default;
}
return $value;
}
}

View File

@@ -0,0 +1,86 @@
<?php
class Jetpack_Sync_Module_Import extends Jetpack_Sync_Module {
private $import_end = false;
public function name() {
return 'import';
}
public function init_listeners( $callable ) {
add_action( 'export_wp', $callable );
add_action( 'jetpack_sync_import_end', $callable, 10, 2 );
// Movable type, RSS, Livejournal
add_action( 'import_done', array( $this, 'sync_import_done' ) );
// WordPress, Blogger, Livejournal, woo tax rate
add_action( 'import_end', array( $this, 'sync_import_end' ) );
}
public function set_defaults() {
$this->import_end = false;
}
public function sync_import_done( $importer ) {
// We already ran an send the import
if ( $this->import_end ) {
return;
}
$importer_name = $this->get_importer_name( $importer );
/**
* Sync Event that tells that the import is finished
*
* @since 5.0.0
*
* $param string $importer
*/
do_action( 'jetpack_sync_import_end', $importer, $importer_name );
$this->import_end = true;
}
public function sync_import_end() {
// We already ran an send the import
if ( $this->import_end ) {
return;
}
$this->import_end = true;
$importer = 'unknown';
$backtrace = wp_debug_backtrace_summary( null, 0, false );
if ( $this->is_importer( $backtrace, 'Blogger_Importer' ) ) {
$importer = 'blogger';
}
if ( 'unknown' === $importer && $this->is_importer( $backtrace, 'WC_Tax_Rate_Importer' ) ) {
$importer = 'woo-tax-rate';
}
if ( 'unknown' === $importer && $this->is_importer( $backtrace, 'WP_Import' ) ) {
$importer = 'wordpress';
}
$importer_name = $this->get_importer_name( $importer );
/** This filter is already documented in sync/class.jetpack-sync-module-posts.php */
do_action( 'jetpack_sync_import_end', $importer, $importer_name );
}
private function get_importer_name( $importer ) {
$importers = get_importers();
return isset( $importers[ $importer ] ) ? $importers[ $importer ][0] : 'Unknown Importer';
}
private function is_importer( $backtrace, $class_name ) {
foreach ( $backtrace as $trace ) {
if ( strpos( $trace, $class_name ) !== false ) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
class Jetpack_Sync_Module_Menus extends Jetpack_Sync_Module {
private $nav_items_just_added = array();
function name() {
return 'menus';
}
public function init_listeners( $callable ) {
add_action( 'wp_create_nav_menu', $callable, 10, 2 );
add_action( 'wp_update_nav_menu', array( $this, 'update_nav_menu' ), 10, 2 );
add_action( 'wp_add_nav_menu_item', array( $this, 'update_nav_menu_add_item' ), 10, 3 );
add_action( 'wp_update_nav_menu_item', array( $this, 'update_nav_menu_update_item' ), 10, 3 );
add_action( 'post_updated', array( $this, 'remove_just_added_menu_item' ), 10, 2 );
add_action( 'jetpack_sync_updated_nav_menu', $callable, 10, 2 );
add_action( 'jetpack_sync_updated_nav_menu_add_item', $callable, 10, 4 );
add_action( 'jetpack_sync_updated_nav_menu_update_item', $callable, 10, 4 );
add_action( 'delete_nav_menu', $callable, 10, 3 );
}
public function update_nav_menu( $menu_id, $menu_data = array() ) {
if ( empty( $menu_data ) ) {
return;
}
/**
* Helps sync log that a nav menu was updated.
*
* @since 5.0.0
*
* @param int $menu_id, the id of the menu
* @param object $menu_data
*/
do_action( 'jetpack_sync_updated_nav_menu', $menu_id, $menu_data );
}
public function update_nav_menu_add_item( $menu_id, $nav_item_id, $nav_item_args ) {
$menu_data = wp_get_nav_menu_object( $menu_id );
$this->nav_items_just_added[] = $nav_item_id;
/**
* Helps sync log that a new menu item was added.
*
* @since 5.0.0
*
* @param int $menu_id, the id of the menu
* @param object $menu_data
* @param int $nav_item_id
* @param int $nav_item_args
*/
do_action( 'jetpack_sync_updated_nav_menu_add_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args );
}
public function update_nav_menu_update_item( $menu_id, $nav_item_id, $nav_item_args ) {
if ( in_array( $nav_item_id, $this->nav_items_just_added ) ) {
return;
}
$menu_data = wp_get_nav_menu_object( $menu_id );
/**
* Helps sync log that an update to the menu item happened.
*
* @since 5.0.0
*
* @param int $menu_id, the id of the menu
* @param object $menu_data
* @param int $nav_item_id
* @param int $nav_item_args
*/
do_action( 'jetpack_sync_updated_nav_menu_update_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args );
}
public function remove_just_added_menu_item( $nav_item_id, $post_after ) {
if ( 'nav_menu_item' !== $post_after->post_type ) {
return;
}
$this->nav_items_just_added = array_diff( $this->nav_items_just_added, array( $nav_item_id ) );
}
}

View File

@@ -0,0 +1,62 @@
<?php
class Jetpack_Sync_Module_Meta extends Jetpack_Sync_Module {
public function name() {
return 'meta';
}
/**
* This implementation of get_objects_by_id() is a bit hacky since we're not passing in an array of meta IDs,
* but instead an array of post or comment IDs for which to retrieve meta for. On top of that,
* we also pass in an associative array where we expect there to be 'meta_key' and 'ids' keys present.
*
* This seemed to be required since if we have missing meta on WP.com and need to fetch it, we don't know what
* the meta key is, but we do know that we have missing meta for a given post or comment.
*
* @param string $object_type The type of object for which we retrieve meta. Either 'post' or 'comment'
* @param array $config Must include 'meta_key' and 'ids' keys
*
* @return array
*/
public function get_objects_by_id( $object_type, $config ) {
global $wpdb;
$table = _get_meta_table( $object_type );
if ( ! $table ) {
return array();
}
if ( ! isset( $config['meta_key'] ) || ! isset( $config['ids'] ) || ! is_array( $config['ids'] ) ) {
return array();
}
$meta_key = $config['meta_key'];
$ids = $config['ids'];
$object_id_column = $object_type.'_id';
// Sanitize so that the array only has integer values
$ids_string = implode( ', ', array_map( 'intval', $ids ) );
$metas = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE {$object_id_column} IN ( {$ids_string} ) AND meta_key = %s",
$meta_key
)
);
$meta_objects = array();
foreach( (array) $metas as $meta_object ) {
$meta_object = (array) $meta_object;
$meta_objects[ $meta_object[ $object_id_column ] ] = array(
'meta_type' => $object_type,
'meta_id' => $meta_object['meta_id'],
'meta_key' => $meta_key,
'meta_value' => $meta_object['meta_value'],
'object_id' => $meta_object[ $object_id_column ],
);
}
return $meta_objects;
}
}

View File

@@ -0,0 +1,113 @@
<?php
class Jetpack_Sync_Module_Network_Options extends Jetpack_Sync_Module {
private $network_options_whitelist;
public function name() {
return 'network_options';
}
public function init_listeners( $callable ) {
if ( ! is_multisite() ) {
return;
}
// multi site network options
add_action( 'add_site_option', $callable, 10, 2 );
add_action( 'update_site_option', $callable, 10, 3 );
add_action( 'delete_site_option', $callable, 10, 1 );
$whitelist_network_option_handler = array( $this, 'whitelist_network_options' );
add_filter( 'jetpack_sync_before_enqueue_delete_site_option', $whitelist_network_option_handler );
add_filter( 'jetpack_sync_before_enqueue_add_site_option', $whitelist_network_option_handler );
add_filter( 'jetpack_sync_before_enqueue_update_site_option', $whitelist_network_option_handler );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_network_options', $callable );
}
public function init_before_send() {
if ( ! is_multisite() ) {
return;
}
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_network_options', array(
$this,
'expand_network_options',
) );
}
public function set_defaults() {
$this->network_options_whitelist = Jetpack_Sync_Defaults::$default_network_options_whitelist;
}
function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
if ( ! is_multisite() ) {
return array( 0, true );
}
/**
* Tells the client to sync all options to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand options (should always be true)
*/
do_action( 'jetpack_full_sync_network_options', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
function estimate_full_sync_actions( $config ) {
if ( ! is_multisite() ) {
return 0;
}
return 1;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_network_options' );
}
function get_all_network_options() {
$options = array();
foreach ( $this->network_options_whitelist as $option ) {
$options[ $option ] = get_site_option( $option );
}
return $options;
}
function set_network_options_whitelist( $options ) {
$this->network_options_whitelist = $options;
}
function get_network_options_whitelist() {
return $this->network_options_whitelist;
}
// reject non-whitelisted network options
function whitelist_network_options( $args ) {
if ( ! $this->is_whitelisted_network_option( $args[0] ) ) {
return false;
}
return $args;
}
function is_whitelisted_network_option( $option ) {
return is_multisite() && in_array( $option, $this->network_options_whitelist );
}
public function expand_network_options( $args ) {
if ( $args[0] ) {
return $this->get_all_network_options();
}
return $args;
}
}

View File

@@ -0,0 +1,177 @@
<?php
class Jetpack_Sync_Module_Options extends Jetpack_Sync_Module {
private $options_whitelist, $options_contentless;
public function name() {
return 'options';
}
public function init_listeners( $callable ) {
// options
add_action( 'added_option', $callable, 10, 2 );
add_action( 'updated_option', $callable, 10, 3 );
add_action( 'deleted_option', $callable, 10, 1 );
// Sync Core Icon: Detect changes in Core's Site Icon and make it syncable.
add_action( 'add_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
add_action( 'update_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
add_action( 'delete_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) );
$whitelist_option_handler = array( $this, 'whitelist_options' );
add_filter( 'jetpack_sync_before_enqueue_deleted_option', $whitelist_option_handler );
add_filter( 'jetpack_sync_before_enqueue_added_option', $whitelist_option_handler );
add_filter( 'jetpack_sync_before_enqueue_updated_option', $whitelist_option_handler );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_options', $callable );
}
public function init_before_send() {
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_options', array( $this, 'expand_options' ) );
}
public function set_defaults() {
$this->update_options_whitelist();
$this->update_options_contentless();
}
public function set_late_default() {
/** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */
$late_options = apply_filters( 'jetpack_options_whitelist', array() );
if ( ! empty( $late_options ) && is_array( $late_options ) ) {
$this->options_whitelist = array_merge( $this->options_whitelist, $late_options );
}
}
function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
/**
* Tells the client to sync all options to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand options (should always be true)
*/
do_action( 'jetpack_full_sync_options', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
public function estimate_full_sync_actions( $config ) {
return 1;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_options' );
}
// Is public so that we don't have to store so much data all the options twice.
function get_all_options() {
$options = array();
$random_string = wp_generate_password();
foreach ( $this->options_whitelist as $option ) {
$option_value = get_option( $option, $random_string );
if ( $option_value !== $random_string ) {
$options[ $option ] = $option_value;
}
}
// add theme mods
$theme_mods_option = 'theme_mods_'.get_option( 'stylesheet' );
$theme_mods_value = get_option( $theme_mods_option, $random_string );
if ( $theme_mods_value === $random_string ) {
return $options;
}
$this->filter_theme_mods( $theme_mods_value );
$options[ $theme_mods_option ] = $theme_mods_value;
return $options;
}
function update_options_whitelist() {
$this->options_whitelist = Jetpack_Sync_Defaults::get_options_whitelist();
}
function set_options_whitelist( $options ) {
$this->options_whitelist = $options;
}
function get_options_whitelist() {
return $this->options_whitelist;
}
function update_options_contentless() {
$this->options_contentless = Jetpack_Sync_Defaults::get_options_contentless();
}
function get_options_contentless() {
return $this->options_contentless;
}
function whitelist_options( $args ) {
// Reject non-whitelisted options
if ( ! $this->is_whitelisted_option( $args[0] ) ) {
return false;
}
// filter our weird array( false ) value for theme_mods_*
if ( 'theme_mods_' === substr( $args[0], 0, 11 ) ) {
$this->filter_theme_mods( $args[1] );
if ( isset( $args[2] ) ) {
$this->filter_theme_mods( $args[2] );
}
}
// Set value(s) of contentless option to empty string(s)
if ( $this->is_contentless_option( $args[0] ) ) {
// Create a new array matching length of $args, containing empty strings
$empty = array_fill( 0, count( $args ), '' );
$empty[0] = $args[0];
return $empty;
}
return $args;
}
function is_whitelisted_option( $option ) {
return in_array( $option, $this->options_whitelist ) || 'theme_mods_' === substr( $option, 0, 11 );
}
private function is_contentless_option( $option ) {
return in_array( $option, $this->options_contentless );
}
private function filter_theme_mods( &$value ) {
if ( is_array( $value ) && isset( $value[0] ) ) {
unset( $value[0] );
}
}
function jetpack_sync_core_icon() {
if ( function_exists( 'get_site_icon_url' ) ) {
$url = get_site_icon_url();
} else {
return;
}
require_once( JETPACK__PLUGIN_DIR . 'modules/site-icon/site-icon-functions.php' );
// If there's a core icon, maybe update the option. If not, fall back to Jetpack's.
if ( ! empty( $url ) && $url !== jetpack_site_icon_url() ) {
// This is the option that is synced with dotcom
Jetpack_Options::update_option( 'site_icon_url', $url );
} else if ( empty( $url ) ) {
Jetpack_Options::delete_option( 'site_icon_url' );
}
}
public function expand_options( $args ) {
if ( $args[0] ) {
return $this->get_all_options();
}
return $args;
}
}

View File

@@ -0,0 +1,281 @@
<?php
class Jetpack_Sync_Module_Plugins extends Jetpack_Sync_Module {
private $action_handler;
private $plugin_info = array();
private $plugins = array();
public function name() {
return 'plugins';
}
public function init_listeners( $callable ) {
$this->action_handler = $callable;
add_action( 'deleted_plugin', array( $this, 'deleted_plugin' ), 10, 2 );
add_action( 'activated_plugin', $callable, 10, 2 );
add_action( 'deactivated_plugin', $callable, 10, 2 );
add_action( 'delete_plugin', array( $this, 'delete_plugin') );
add_filter( 'upgrader_pre_install', array( $this, 'populate_plugins' ), 10, 1 );
add_action( 'upgrader_process_complete', array( $this, 'on_upgrader_completion' ), 10, 2 );
add_action( 'jetpack_plugin_installed', $callable, 10, 1 );
add_action( 'jetpack_plugin_update_failed', $callable, 10, 4 );
add_action( 'jetpack_plugins_updated', $callable, 10, 2 );
add_action( 'admin_action_update', array( $this, 'check_plugin_edit') );
add_action( 'jetpack_edited_plugin', $callable, 10, 2 );
add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'plugin_edit_ajax' ), 0 );
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_activated_plugin', array( $this, 'expand_plugin_data' ) );
add_filter( 'jetpack_sync_before_send_deactivated_plugin', array( $this, 'expand_plugin_data' ) );
//Note that we don't simply 'expand_plugin_data' on the 'delete_plugin' action here because the plugin file is deleted when that action finishes
}
public function populate_plugins( $response ) {
$this->plugins = get_plugins();
return $response;
}
public function on_upgrader_completion( $upgrader, $details ) {
if ( ! isset( $details['type'] ) ) {
return;
}
if ( 'plugin' != $details['type'] ) {
return;
}
if ( ! isset( $details['action'] ) ) {
return;
}
$plugins = ( isset( $details['plugins'] ) ? $details['plugins'] : null );
if ( empty( $plugins ) ) {
$plugins = ( isset( $details['plugin'] ) ? array( $details['plugin'] ) : null );
}
// for plugin installer
if ( empty( $plugins ) && method_exists( $upgrader, 'plugin_info' ) ) {
$plugins = array( $upgrader->plugin_info() );
}
if ( empty( $plugins ) ) {
return; // We shouldn't be here
}
switch ( $details['action'] ) {
case 'update':
$state = array(
'is_autoupdate' => Jetpack_Constants::is_true( 'JETPACK_PLUGIN_AUTOUPDATE' ),
);
$errors = $this->get_errors( $upgrader->skin );
if ( $errors ) {
foreach ( $plugins as $slug ) {
/**
* Sync that a plugin update failed
*
* @since 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'], $state );
}
return;
}
/**
* Sync that a plugin update
*
* @since 5.8.0
*
* @module sync
*
* @param array () $plugin, Plugin Data
*/
do_action( 'jetpack_plugins_updated', array_map( array( $this, 'get_plugin_info' ), $plugins ), $state );
break;
case 'install':
}
if ( 'install' === $details['action'] ) {
/**
* 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 5.8.0
*
* @module sync
*
* @param array () $plugin, Plugin Data
*/
do_action( 'jetpack_plugin_installed', array_map( array( $this, 'get_plugin_info' ), $plugins ) );
return;
}
}
private function get_plugin_info( $slug ) {
$plugins = get_plugins(); // Get the most up to date info
if ( isset( $plugins[ $slug ] ) ) {
return array_merge( array( 'slug' => $slug ), $plugins[ $slug ] );
};
// Try grabbing the info from before the update
return isset( $this->plugins[ $slug ] ) ? array_merge( array( 'slug' => $slug ), $this->plugins[ $slug ] ): array( 'slug' => $slug );
}
private function get_errors( $skin ) {
$errors = method_exists( $skin, 'get_errors' ) ? $skin->get_errors() : null;
if ( is_wp_error( $errors ) ) {
$error_code = $errors->get_error_code();
if ( ! empty( $error_code ) ) {
return array( 'code' => $error_code, 'message' => $errors->get_error_message() );
}
}
if ( isset( $skin->result ) ) {
$errors = $skin->result;
if ( is_wp_error( $errors ) ) {
return array( 'code' => $errors->get_error_code(), 'message' => $errors->get_error_message() );
}
if ( false == $skin->result ) {
return array( 'code' => 'unknown', 'message' => __( 'Unknown Plugin Update Failure', 'jetpack' ) );
}
}
return false;
}
public function check_plugin_edit() {
$screen = get_current_screen();
if ( 'plugin-editor' !== $screen->base ||
! isset( $_POST['newcontent'] ) ||
! isset( $_POST['plugin'] )
) {
return;
}
$plugin = $_POST['plugin'];
$plugins = get_plugins();
if ( ! isset( $plugins[ $plugin ] ) ) {
return;
}
/**
* Helps Sync log that a plugin was edited
*
* @since 4.9.0
*
* @param string $plugin, Plugin slug
* @param mixed $plugins[ $plugin ], Array of plugin data
*/
do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] );
}
public function plugin_edit_ajax() {
// this validation is based on wp_edit_theme_plugin_file()
$args = wp_unslash( $_POST );
if ( empty( $args['file'] ) ) {
return;
}
$file = $args['file'];
if ( 0 !== validate_file( $file ) ) {
return;
}
if ( ! isset( $args['newcontent'] ) ) {
return;
}
if ( ! isset( $args['nonce'] ) ) {
return;
}
if ( empty( $args['plugin'] ) ) {
return;
}
$plugin = $args['plugin'];
if ( ! current_user_can( 'edit_plugins' ) ) {
return;
}
if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
return;
}
$plugins = get_plugins();
if ( ! array_key_exists( $plugin, $plugins ) ) {
return;
}
if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
return;
}
$real_file = WP_PLUGIN_DIR . '/' . $file;
if ( ! is_writeable( $real_file ) ) {
return;
}
$file_pointer = fopen( $real_file, 'w+' );
if ( false === $file_pointer ) {
return;
}
fclose( $file_pointer );
/**
* This action is documented already in this file
*/
do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] );
}
public function delete_plugin( $plugin_path ) {
$full_plugin_path = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_path;
//Checking for file existence because some sync plugin module tests simulate plugin installation and deletion without putting file on disk
if ( file_exists( $full_plugin_path ) ) {
$all_plugin_data = get_plugin_data( $full_plugin_path );
$data = array(
'name' => $all_plugin_data['Name'],
'version' => $all_plugin_data['Version'],
);
} else {
$data = array(
'name' => $plugin_path,
'version' => 'unknown',
);
}
$this->plugin_info[ $plugin_path ] = $data;
}
public function deleted_plugin( $plugin_path, $is_deleted ) {
call_user_func( $this->action_handler, $plugin_path, $is_deleted, $this->plugin_info[ $plugin_path ] );
unset( $this->plugin_info[ $plugin_path ] );
}
public function expand_plugin_data( $args ) {
$plugin_path = $args[0];
$plugin_data = array();
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
if ( isset( $all_plugins[ $plugin_path ] ) ) {
$all_plugin_data = $all_plugins[ $plugin_path ];
$plugin_data['name'] = $all_plugin_data['Name'];
$plugin_data['version'] = $all_plugin_data['Version'];
}
return array(
$args[0],
$args[1],
$plugin_data,
);
}
}

View File

@@ -0,0 +1,383 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
class Jetpack_Sync_Module_Posts extends Jetpack_Sync_Module {
private $just_published = array();
private $previous_status = array();
private $action_handler;
private $import_end = false;
const DEFAULT_PREVIOUS_STATE = 'new';
public function name() {
return 'posts';
}
public function get_object_by_id( $object_type, $id ) {
if ( $object_type === 'post' && $post = get_post( intval( $id ) ) ) {
return $this->filter_post_content_and_add_links( $post );
}
return false;
}
public function init_listeners( $callable ) {
$this->action_handler = $callable;
// Core < 4.7 doesn't deal with nested wp_insert_post calls very well
global $wp_version;
$priority = version_compare( $wp_version, '4.7-alpha', '<' ) ? 0 : 11;
add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ), $priority, 3 );
add_action( 'jetpack_sync_save_post', $callable, 10, 4 );
add_action( 'deleted_post', $callable, 10 );
add_action( 'jetpack_published_post', $callable, 10, 2 );
add_action( 'transition_post_status', array( $this, 'save_published' ), 10, 3 );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_blacklisted_post_types' ) );
// listen for meta changes
$this->init_listeners_for_meta_type( 'post', $callable );
$this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) );
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 );
}
public function daily_akismet_meta_cleanup_before( $feedback_ids ) {
remove_action( 'deleted_post_meta', $this->action_handler );
/**
* Used for syncing deletion of batch post meta
*
* @since 6.1.0
*
* @module sync
*
* $param array $feedback_ids feedback post IDs
* $param string $meta_key to be deleted
*/
do_action( 'jetpack_post_meta_batch_delete', $feedback_ids, '_feedback_akismet_values');
}
public function daily_akismet_meta_cleanup_after( $feedback_ids ) {
add_action( 'deleted_post_meta', $this->action_handler );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_posts', $callable ); // also sends post meta
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_sync_save_post', array( $this, 'expand_jetpack_sync_save_post' ) );
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $wpdb->posts WHERE " . $this->get_where_sql( $config );
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
private function get_where_sql( $config ) {
$where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
// config is a list of post IDs to sync
if ( is_array( $config ) ) {
$where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return $where_sql;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_posts' );
}
/**
* Process content before send
*
* @param array $args wp_insert_post arguments
*
* @return array
*/
function expand_jetpack_sync_save_post( $args ) {
list( $post_id, $post, $update, $previous_state ) = $args;
return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state );
}
function filter_blacklisted_post_types( $args ) {
$post = $args[1];
if ( in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) {
return false;
}
return $args;
}
// Meta
function filter_meta( $args ) {
if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) {
return $args;
}
return false;
}
function is_whitelisted_post_meta( $meta_key ) {
// _wpas_skip_ is used by publicize
return in_array( $meta_key, Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ) || wp_startswith( $meta_key, '_wpas_skip_' );
}
function is_post_type_allowed( $post_id ) {
$post = get_post( intval( $post_id ) );
if( $post->post_type ) {
return ! in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) );
}
return false;
}
function remove_embed() {
global $wp_embed;
remove_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
// remove the embed shortcode since we would do the part later.
remove_shortcode( 'embed' );
// Attempts to embed all URLs in a post
remove_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
}
function add_embed() {
global $wp_embed;
add_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 );
// Shortcode placeholder for strip_shortcodes()
add_shortcode( 'embed', '__return_false' );
// Attempts to embed all URLs in a post
add_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 );
}
// Expands wp_insert_post to include filtered content
function filter_post_content_and_add_links( $post_object ) {
global $post;
$post = $post_object;
// return non existant post
$post_type = get_post_type_object( $post->post_type );
if ( empty( $post_type ) || ! is_object( $post_type ) ) {
$non_existant_post = new stdClass();
$non_existant_post->ID = $post->ID;
$non_existant_post->post_modified = $post->post_modified;
$non_existant_post->post_modified_gmt = $post->post_modified_gmt;
$non_existant_post->post_status = 'jetpack_sync_non_registered_post_type';
return $non_existant_post;
}
/**
* Filters whether to prevent sending post data to .com
*
* Passing true to the filter will prevent the post data from being sent
* to the WordPress.com.
* Instead we pass data that will still enable us to do a checksum against the
* Jetpacks data but will prevent us from displaying the data on in the API as well as
* other services.
* @since 4.2.0
*
* @param boolean false prevent post data from being synced to WordPress.com
* @param mixed $post WP_POST object
*/
if ( apply_filters( 'jetpack_sync_prevent_sending_post_data', false, $post ) ) {
// We only send the bare necessary object to be able to create a checksum.
$blocked_post = new stdClass();
$blocked_post->ID = $post->ID;
$blocked_post->post_modified = $post->post_modified;
$blocked_post->post_modified_gmt = $post->post_modified_gmt;
$blocked_post->post_status = 'jetpack_sync_blocked';
return $blocked_post;
}
// lets not do oembed just yet.
$this->remove_embed();
if ( 0 < strlen( $post->post_password ) ) {
$post->post_password = 'auto-' . wp_generate_password( 10, false );
}
/** This filter is already documented in core. wp-includes/post-template.php */
if ( Jetpack_Sync_Settings::get_setting( 'render_filtered_content' ) && $post_type->public ) {
global $shortcode_tags;
/**
* Filter prevents some shortcodes from expanding.
*
* Since we can can expand some type of shortcode better on the .com side and make the
* expansion more relevant to contexts. For example [galleries] and subscription emails
*
* @since 4.5.0
*
* @param array of shortcode tags to remove.
*/
$shortcodes_to_remove = apply_filters( 'jetpack_sync_do_not_expand_shortcodes', array(
'gallery',
'slideshow'
) );
$removed_shortcode_callbacks = array();
foreach ( $shortcodes_to_remove as $shortcode ) {
if ( isset ( $shortcode_tags[ $shortcode ] ) ) {
$removed_shortcode_callbacks[ $shortcode ] = $shortcode_tags[ $shortcode ];
}
}
array_map( 'remove_shortcode', array_keys( $removed_shortcode_callbacks ) );
$post->post_content_filtered = apply_filters( 'the_content', $post->post_content );
$post->post_excerpt_filtered = apply_filters( 'the_excerpt', $post->post_excerpt );
foreach ( $removed_shortcode_callbacks as $shortcode => $callback ) {
add_shortcode( $shortcode, $callback );
}
}
$this->add_embed();
if ( has_post_thumbnail( $post->ID ) ) {
$image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
$post->featured_image = $image_attributes[0];
}
}
$post->permalink = get_permalink( $post->ID );
$post->shortlink = wp_get_shortlink( $post->ID );
if ( function_exists( 'amp_get_permalink' ) ) {
$post->amp_permalink = amp_get_permalink( $post->ID );
}
return $post;
}
public function save_published( $new_status, $old_status, $post ) {
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
$this->just_published[ $post->ID ] = true;
}
$this->previous_status[ $post->ID ] = $old_status;
}
public function wp_insert_post( $post_ID, $post = null, $update = null ) {
if ( ! is_numeric( $post_ID ) || is_null( $post ) ) {
return;
}
// workaround for https://github.com/woocommerce/woocommerce/issues/18007
if ( $post && 'shop_order' === $post->post_type ) {
$post = get_post( $post_ID );
}
$previous_status = isset( $this->previous_status[ $post_ID ] ) ?
$this->previous_status[ $post_ID ] :
self::DEFAULT_PREVIOUS_STATE;
$just_published = isset( $this->just_published[ $post_ID ] ) ?
$this->just_published[ $post_ID ] :
false;
$state = array(
'is_auto_save' => (bool) Jetpack_Constants::get_constant( 'DOING_AUTOSAVE' ),
'previous_status' => $previous_status,
'just_published' => $just_published
);
/**
* Filter that is used to add to the post flags ( meta data ) when a post gets published
*
* @since 5.8.0
*
* @param int $post_ID the post ID
* @param mixed $post WP_POST object
* @param bool $update Whether this is an existing post being updated or not.
* @param mixed $state state
*
* @module sync
*/
do_action( 'jetpack_sync_save_post', $post_ID, $post, $update, $state );
unset( $this->previous_status[ $post_ID ] );
$this->send_published( $post_ID, $post );
}
public function send_published( $post_ID, $post ) {
if ( ! isset( $this->just_published[ $post_ID ] ) ) {
return;
}
// Post revisions cause race conditions where this send_published add the action before the actual post gets synced
if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post ) ) {
return;
}
$post_flags = array(
'post_type' => $post->post_type
);
$author_user_object = get_user_by( 'id', $post->post_author );
if ( $author_user_object ) {
$post_flags['author'] = array(
'id' => $post->post_author,
'wpcom_user_id' => get_user_meta( $post->post_author, 'wpcom_user_id', true ),
'display_name' => $author_user_object->display_name,
'email' => $author_user_object->user_email,
'translated_role' => Jetpack::translate_user_to_role( $author_user_object ),
);
}
/**
* Filter that is used to add to the post flags ( meta data ) when a post gets published
*
* @since 4.4.0
*
* @param mixed array post flags that are added to the post
* @param mixed $post WP_POST object
*/
$flags = apply_filters( 'jetpack_published_post_flags', $post_flags, $post );
/**
* Action that gets synced when a post type gets published.
*
* @since 4.4.0
*
* @param int $post_ID
* @param mixed array $flags post flags that are added to the post
*/
do_action( 'jetpack_published_post', $post_ID, $flags );
unset( $this->just_published[ $post_ID ] );
}
public function expand_post_ids( $args ) {
$post_ids = $args[0];
$posts = array_filter( array_map( array( 'WP_Post', 'get_instance' ), $post_ids ) );
$posts = array_map( array( $this, 'filter_post_content_and_add_links' ), $posts );
$posts = array_values( $posts ); // reindex in case posts were deleted
return array(
$posts,
$this->get_metadata( $post_ids, 'post', Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ),
$this->get_term_relationships( $post_ids ),
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* logs bruteprotect failed logins via sync
*/
class Jetpack_Sync_Module_Protect extends Jetpack_Sync_Module {
function name() {
return 'protect';
}
function init_listeners( $callback ) {
add_action( 'jpp_log_failed_attempt', array( $this, 'maybe_log_failed_login_attempt' ) );
add_action( 'jetpack_valid_failed_login_attempt', $callback );
}
function maybe_log_failed_login_attempt( $failed_attempt ) {
$protect = Jetpack_Protect_Module::instance();
if ( $protect->has_login_ability() && ! Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) ) {
do_action( 'jetpack_valid_failed_login_attempt', $failed_attempt );
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
class Jetpack_Sync_Module_Stats extends Jetpack_Sync_Module {
function name() {
return 'stats';
}
function init_listeners( $callback ) {
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.
*/
public function sync_site_stats() {
do_action( 'jetpack_sync_heartbeat_stats' );
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_sync_heartbeat_stats', array( $this, 'add_stats' ) );
}
public function add_stats() {
return array( Jetpack::get_stat_data( false, false ) );
}
}

View File

@@ -0,0 +1,129 @@
<?php
class Jetpack_Sync_Module_Terms extends Jetpack_Sync_Module {
private $taxonomy_whitelist;
function name() {
return 'terms';
}
function init_listeners( $callable ) {
add_action( 'created_term', array( $this, 'save_term_handler' ), 10, 3 );
add_action( 'edited_term', array( $this, 'save_term_handler' ), 10, 3 );
add_action( 'jetpack_sync_save_term', $callable );
add_action( 'jetpack_sync_add_term', $callable );
add_action( 'delete_term', $callable, 10, 4 );
add_action( 'set_object_terms', $callable, 10, 6 );
add_action( 'deleted_term_relationships', $callable, 10, 2 );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_terms', $callable, 10, 2 );
}
function init_before_send() {
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_terms', array( $this, 'expand_term_ids' ) );
}
function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
global $wpdb;
// TODO: process state
$taxonomies = get_taxonomies();
$total_chunks_counter = 0;
foreach ( $taxonomies as $taxonomy ) {
// I hope this is never bigger than RAM...
$term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s", $taxonomy ) ); // Should we set a limit here?
// Request posts in groups of N for efficiency
$chunked_term_ids = array_chunk( $term_ids, self::ARRAY_CHUNK_SIZE );
// Send each chunk as an array of objects
foreach ( $chunked_term_ids as $chunk ) {
do_action( 'jetpack_full_sync_terms', $chunk, $taxonomy );
$total_chunks_counter ++;
}
}
return array( $total_chunks_counter, true );
}
function estimate_full_sync_actions( $config ) {
// TODO - make this (and method above) more efficient for large numbers of terms or taxonomies
global $wpdb;
$taxonomies = get_taxonomies();
$total_chunks_counter = 0;
foreach ( $taxonomies as $taxonomy ) {
$total_ids = $wpdb->get_var( $wpdb->prepare( "SELECT count(term_id) FROM $wpdb->term_taxonomy WHERE taxonomy = %s", $taxonomy ) );
$total_chunks_counter += (int) ceil( $total_ids / self::ARRAY_CHUNK_SIZE );
}
return $total_chunks_counter;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_terms' );
}
function save_term_handler( $term_id, $tt_id, $taxonomy ) {
if ( class_exists( 'WP_Term' ) ) {
$term_object = WP_Term::get_instance( $term_id, $taxonomy );
} else {
$term_object = get_term_by( 'id', $term_id, $taxonomy );
}
$current_filter = current_filter();
if ( 'created_term' === $current_filter ) {
/**
* Fires when the client needs to add a new term
*
* @since 5.0.0
*
* @param object the Term object
*/
do_action( 'jetpack_sync_add_term', $term_object );
return;
}
/**
* Fires when the client needs to update a term
*
* @since 4.2.0
*
* @param object the Term object
*/
do_action( 'jetpack_sync_save_term', $term_object );
}
function set_taxonomy_whitelist( $taxonomies ) {
$this->taxonomy_whitelist = $taxonomies;
}
function set_defaults() {
$this->taxonomy_whitelist = Jetpack_Sync_Defaults::$default_taxonomy_whitelist;
}
public function expand_term_ids( $args ) {
global $wp_version;
$term_ids = $args[0];
$taxonomy = $args[1];
// version 4.5 or higher
if ( version_compare( $wp_version, 4.5, '>=' ) ) {
$terms = get_terms( array(
'taxonomy' => $taxonomy,
'hide_empty' => false,
'include' => $term_ids,
) );
} else {
$terms = get_terms( $taxonomy, array(
'hide_empty' => false,
'include' => $term_ids,
) );
}
return $terms;
}
}

View File

@@ -0,0 +1,597 @@
<?php
class Jetpack_Sync_Module_Themes extends Jetpack_Sync_Module {
function name() {
return 'themes';
}
public function init_listeners( $callable ) {
add_action( 'switch_theme', array( $this, 'sync_theme_support' ) );
add_action( 'jetpack_sync_current_theme_support', $callable );
add_action( 'upgrader_process_complete', array( $this, 'check_upgrader'), 10, 2 );
add_action( 'jetpack_installed_theme', $callable, 10, 2 );
add_action( 'jetpack_updated_themes', $callable, 10, 2 );
add_action( 'delete_site_transient_update_themes', array( $this, 'detect_theme_deletion') );
add_action( 'jetpack_deleted_theme', $callable, 10, 2 );
add_filter( 'wp_redirect', array( $this, 'detect_theme_edit' ) );
add_action( 'jetpack_edited_theme', $callable, 10, 2 );
add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'theme_edit_ajax' ), 0 );
add_action( 'update_site_option_allowedthemes', array( $this, 'sync_network_allowed_themes_change' ), 10, 4 );
add_action( 'jetpack_network_disabled_themes', $callable, 10, 2 );
add_action( 'jetpack_network_enabled_themes', $callable, 10, 2 );
// Sidebar updates.
add_action( 'update_option_sidebars_widgets', array( $this, 'sync_sidebar_widgets_actions' ), 10, 2 );
add_action( 'jetpack_widget_added', $callable, 10, 4 );
add_action( 'jetpack_widget_removed', $callable, 10, 4 );
add_action( 'jetpack_widget_moved_to_inactive', $callable, 10, 2 );
add_action( 'jetpack_cleared_inactive_widgets', $callable );
add_action( 'jetpack_widget_reordered', $callable, 10, 2 );
add_filter( 'widget_update_callback', array( $this, 'sync_widget_edit' ), 10, 4 );
add_action( 'jetpack_widget_edited', $callable );
}
public function sync_widget_edit( $instance, $new_instance, $old_instance, $widget_object ) {
if ( empty( $old_instance ) ) {
return $instance;
}
$widget = array(
'name' => $widget_object->name,
'id' => $widget_object->id,
'title' => isset( $new_instance['title'] ) ? $new_instance['title'] : '',
);
/**
* Trigger action to alert $callable sync listener that a widget was edited
*
* @since 5.0.0
*
* @param string $widget_name , Name of edited widget
*/
do_action( 'jetpack_widget_edited', $widget );
return $instance;
}
public function sync_network_allowed_themes_change( $option, $value, $old_value, $network_id ) {
$all_enabled_theme_slugs = array_keys( $value );
if ( count( $old_value ) > count( $value ) ) {
//Suppress jetpack_network_disabled_themes sync action when theme is deleted
$delete_theme_call = $this->get_delete_theme_call();
if ( ! empty( $delete_theme_call ) ) {
return;
}
$newly_disabled_theme_names = array_keys( array_diff_key( $old_value, $value ) );
$newly_disabled_themes = $this->get_theme_details_for_slugs( $newly_disabled_theme_names );
/**
* Trigger action to alert $callable sync listener that network themes were disabled
*
* @since 5.0.0
*
* @param mixed $newly_disabled_themes, Array of info about network disabled themes
* @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
*/
do_action( 'jetpack_network_disabled_themes', $newly_disabled_themes, $all_enabled_theme_slugs );
return;
}
$newly_enabled_theme_names = array_keys( array_diff_key( $value, $old_value ) );
$newly_enabled_themes = $this->get_theme_details_for_slugs( $newly_enabled_theme_names );
/**
* Trigger action to alert $callable sync listener that network themes were enabled
*
* @since 5.0.0
*
* @param mixed $newly_enabled_themes , Array of info about network enabled themes
* @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes
*/
do_action( 'jetpack_network_enabled_themes', $newly_enabled_themes, $all_enabled_theme_slugs );
}
private function get_theme_details_for_slugs( $theme_slugs ) {
$theme_data = array();
foreach ( $theme_slugs as $slug ) {
$theme = wp_get_theme( $slug );
$theme_data[ $slug ] = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
'slug' => $slug,
);
}
return $theme_data;
}
public function detect_theme_edit( $redirect_url ) {
$url = wp_parse_url( admin_url( $redirect_url ) );
$theme_editor_url = wp_parse_url( admin_url( 'theme-editor.php' ) );
if ( $theme_editor_url['path'] !== $url['path'] ) {
return $redirect_url;
}
$query_params = array();
wp_parse_str( $url['query'], $query_params );
if (
! isset( $_POST['newcontent'] ) ||
! isset( $query_params['file'] ) ||
! isset( $query_params['theme'] ) ||
! isset( $query_params['updated'] )
) {
return $redirect_url;
}
$theme = wp_get_theme( $query_params['theme'] );
$theme_data = array(
'name' => $theme->get('Name'),
'version' => $theme->get('Version'),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* Trigger action to alert $callable sync listener that a theme was edited
*
* @since 5.0.0
*
* @param string $query_params['theme'], Slug of edited theme
* @param string $theme_data, Information about edited them
*/
do_action( 'jetpack_edited_theme', $query_params['theme'], $theme_data );
return $redirect_url;
}
public function theme_edit_ajax() {
$args = wp_unslash( $_POST );
if ( empty( $args['theme'] ) ) {
return;
}
if ( empty( $args['file'] ) ) {
return;
}
$file = $args['file'];
if ( 0 !== validate_file( $file ) ) {
return;
}
if ( ! isset( $args['newcontent'] ) ) {
return;
}
if ( ! isset( $args['nonce'] ) ) {
return;
}
$stylesheet = $args['theme'];
if ( 0 !== validate_file( $stylesheet ) ) {
return;
}
if ( ! current_user_can( 'edit_themes' ) ) {
return;
}
$theme = wp_get_theme( $stylesheet );
if ( ! $theme->exists() ) {
return;
}
$real_file = $theme->get_stylesheet_directory() . '/' . $file;
if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) {
return;
}
if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
return;
}
$editable_extensions = wp_get_theme_file_editable_extensions( $theme );
$allowed_files = array();
foreach ( $editable_extensions as $type ) {
switch ( $type ) {
case 'php':
$allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
break;
case 'css':
$style_files = $theme->get_files( 'css', -1 );
$allowed_files['style.css'] = $style_files['style.css'];
$allowed_files = array_merge( $allowed_files, $style_files );
break;
default:
$allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
break;
}
}
if ( 0 !== validate_file( $real_file, $allowed_files ) ) {
return;
}
// Ensure file is real.
if ( ! is_file( $real_file ) ) {
return;
}
// Ensure file extension is allowed.
$extension = null;
if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
$extension = strtolower( $matches[1] );
if ( ! in_array( $extension, $editable_extensions, true ) ) {
return;
}
}
if ( ! is_writeable( $real_file ) ) {
return;
}
$file_pointer = fopen( $real_file, 'w+' );
if ( false === $file_pointer ) {
return;
}
fclose( $file_pointer );
$theme_data = array(
'name' => $theme->get('Name'),
'version' => $theme->get('Version'),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* This action is documented already in this file
*/
do_action( 'jetpack_edited_theme', $stylesheet, $theme_data );
}
public function detect_theme_deletion() {
$delete_theme_call = $this->get_delete_theme_call();
if ( empty( $delete_theme_call ) ) {
return;
}
$slug = $delete_theme_call['args'][0];
$theme = wp_get_theme( $slug );
$theme_data = array(
'name' => $theme->get('Name'),
'version' => $theme->get('Version'),
'uri' => $theme->get( 'ThemeURI' ),
'slug' => $slug,
);
/**
* Signals to the sync listener that a theme was deleted and a sync action
* reflecting the deletion and theme slug should be sent
*
* @since 5.0.0
*
* @param string $slug Theme slug
* @param array $theme_data Theme info Since 5.3
*/
do_action( 'jetpack_deleted_theme', $slug, $theme_data );
}
public function check_upgrader( $upgrader, $details ) {
if ( ! isset( $details['type'] ) ||
'theme' !== $details['type'] ||
is_wp_error( $upgrader->skin->result ) ||
! method_exists( $upgrader, 'theme_info' )
) {
return;
}
if ( 'install' === $details['action'] ) {
$theme = $upgrader->theme_info();
if ( ! $theme instanceof WP_Theme ) {
return;
}
$theme_info = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
);
/**
* Signals to the sync listener that a theme was installed and a sync action
* reflecting the installation and the theme info should be sent
*
* @since 4.9.0
*
* @param string $theme->theme_root Text domain of the theme
* @param mixed $theme_info Array of abbreviated theme info
*/
do_action( 'jetpack_installed_theme', $theme->stylesheet, $theme_info );
}
if ( 'update' === $details['action'] ) {
$themes = array();
if ( empty( $details['themes'] ) && isset ( $details['theme'] ) ) {
$details['themes'] = array( $details['theme'] );
}
foreach ( $details['themes'] as $theme_slug ) {
$theme = wp_get_theme( $theme_slug );
if ( ! $theme instanceof WP_Theme ) {
continue;
}
$themes[ $theme_slug ] = array(
'name' => $theme->get( 'Name' ),
'version' => $theme->get( 'Version' ),
'uri' => $theme->get( 'ThemeURI' ),
'stylesheet' => $theme->stylesheet,
);
}
if ( empty( $themes ) ) {
return;
}
/**
* Signals to the sync listener that one or more themes was updated and a sync action
* reflecting the update and the theme info should be sent
*
* @since 6.2.0
*
* @param mixed $themes Array of abbreviated theme info
*/
do_action( 'jetpack_updated_themes', $themes );
}
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_theme_data', $callable );
}
public function sync_theme_support() {
/**
* Fires when the client needs to sync theme support info
* Only sends theme support attributes whitelisted in Jetpack_Sync_Defaults::$default_theme_support_whitelist
*
* @since 4.2.0
*
* @param object the theme support hash
*/
do_action( 'jetpack_sync_current_theme_support' , $this->get_theme_support_info() );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
/**
* Tells the client to sync all theme data to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand theme data (should always be true)
*/
do_action( 'jetpack_full_sync_theme_data', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
public function estimate_full_sync_actions( $config ) {
return 1;
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_theme_data', array( $this, 'expand_theme_data' ) );
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_theme_data' );
}
function expand_theme_data() {
return array( $this->get_theme_support_info() );
}
function get_widget_name( $widget_id ) {
global $wp_registered_widgets;
return ( isset( $wp_registered_widgets[ $widget_id ] ) ? $wp_registered_widgets[ $widget_id ]['name'] : null );
}
function get_sidebar_name( $sidebar_id ) {
global $wp_registered_sidebars;
return ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : null );
}
function sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ) {
$added_widgets = array_diff( $new_widgets, $old_widgets );
if ( empty( $added_widgets ) ) {
return array();
}
$moved_to_sidebar = array();
$sidebar_name = $this->get_sidebar_name( $sidebar );
//Don't sync jetpack_widget_added if theme was switched
if ( $this->is_theme_switch() ) {
return array();
}
foreach ( $added_widgets as $added_widget ) {
$moved_to_sidebar[] = $added_widget;
$added_widget_name = $this->get_widget_name( $added_widget );
/**
* Helps Sync log that a widget got added
*
* @since 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $added_widget, Widget id got added
* @param string $sidebar_name, Sidebar id got changed Since 5.0.0
* @param string $added_widget_name, Widget id got added Since 5.0.0
*
*/
do_action( 'jetpack_widget_added', $sidebar, $added_widget, $sidebar_name, $added_widget_name );
}
return $moved_to_sidebar;
}
function sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $inactive_widgets ) {
$removed_widgets = array_diff( $old_widgets, $new_widgets );
if ( empty( $removed_widgets ) ) {
return array();
}
$moved_to_inactive = array();
$sidebar_name = $this->get_sidebar_name( $sidebar );
foreach( $removed_widgets as $removed_widget ) {
// Lets check if we didn't move the widget to in_active_widgets
if ( isset( $inactive_widgets ) && ! in_array( $removed_widget, $inactive_widgets ) ) {
$removed_widget_name = $this->get_widget_name( $removed_widget );
/**
* Helps Sync log that a widgte got removed
*
* @since 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $removed_widget, Widget id got removed
* @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
* @param string $removed_widget_name, Name of the widget that got removed Since 5.0.0
*/
do_action( 'jetpack_widget_removed', $sidebar, $removed_widget, $sidebar_name, $removed_widget_name );
} else {
$moved_to_inactive[] = $removed_widget;
}
}
return $moved_to_inactive;
}
function sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ) {
$added_widgets = array_diff( $new_widgets, $old_widgets );
if ( ! empty( $added_widgets ) ) {
return;
}
$removed_widgets = array_diff( $old_widgets, $new_widgets );
if ( ! empty( $removed_widgets ) ) {
return;
}
if ( serialize( $old_widgets ) !== serialize( $new_widgets ) ) {
$sidebar_name = $this->get_sidebar_name( $sidebar );
/**
* Helps Sync log that a sidebar id got reordered
*
* @since 4.9.0
*
* @param string $sidebar, Sidebar id got changed
* @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0
*/
do_action( 'jetpack_widget_reordered', $sidebar, $sidebar_name );
}
}
function sync_sidebar_widgets_actions( $old_value, $new_value ) {
// Don't really know how to deal with different array_values yet.
if (
( isset( $old_value['array_version'] ) && $old_value['array_version'] !== 3 ) ||
( isset( $new_value['array_version'] ) && $new_value['array_version'] !== 3 )
) {
return;
}
$moved_to_inactive_ids = array();
$moved_to_sidebar = array();
foreach ( $new_value as $sidebar => $new_widgets ) {
if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ) ) ) {
continue;
}
$old_widgets = isset( $old_value[ $sidebar ] )
? $old_value[ $sidebar ]
: array();
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_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 );
$moved_to_sidebar = array_merge( $moved_to_sidebar, $moved_to_sidebar_recently );
$this->sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar );
}
//Don't sync either jetpack_widget_moved_to_inactive or jetpack_cleared_inactive_widgets if theme was switched
if ( $this->is_theme_switch() ) {
return;
}
// Treat inactive sidebar a bit differently
if ( ! empty( $moved_to_inactive_ids ) ) {
$moved_to_inactive_name = array_map( array( $this, 'get_widget_name' ), $moved_to_inactive_ids );
/**
* Helps Sync log that a widgets IDs got moved to in active
*
* @since 4.9.0
*
* @param array $moved_to_inactive_ids, Array of widgets id that moved to inactive id got changed
* @param array $moved_to_inactive_names, Array of widgets names that moved to inactive id got changed Since 5.0.0
*/
do_action( 'jetpack_widget_moved_to_inactive', $moved_to_inactive_ids, $moved_to_inactive_name );
} elseif ( empty( $moved_to_sidebar ) &&
empty( $new_value['wp_inactive_widgets']) &&
! empty( $old_value['wp_inactive_widgets'] ) ) {
/**
* Helps Sync log that a got cleared from inactive.
*
* @since 4.9.0
*/
do_action( 'jetpack_cleared_inactive_widgets' );
}
}
private function get_theme_support_info() {
global $_wp_theme_features;
$theme_support = array();
foreach ( Jetpack_Sync_Defaults::$default_theme_support_whitelist as $theme_feature ) {
$has_support = current_theme_supports( $theme_feature );
if ( $has_support ) {
$theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ];
}
}
$theme = wp_get_theme();
$theme_support['name'] = $theme->get('Name');
$theme_support['version'] = $theme->get('Version');
$theme_support['slug'] = $theme->get_stylesheet();
$theme_support['uri'] = $theme->get('ThemeURI');
return $theme_support;
}
private function get_delete_theme_call() {
$backtrace = debug_backtrace();
$delete_theme_call = null;
foreach ( $backtrace as $call ) {
if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) {
$delete_theme_call = $call;
break;
}
}
return $delete_theme_call;
}
private function is_theme_switch() {
return did_action( 'after_switch_theme' );
}
}

View File

@@ -0,0 +1,282 @@
<?php
class Jetpack_Sync_Module_Updates extends Jetpack_Sync_Module {
const UPDATES_CHECKSUM_OPTION_NAME = 'jetpack_updates_sync_checksum';
private $old_wp_version = null;
function name() {
return 'updates';
}
public function init_listeners( $callable ) {
global $wp_version;
$this->old_wp_version = $wp_version;
add_action( 'set_site_transient_update_plugins', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'set_site_transient_update_themes', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'set_site_transient_update_core', array( $this, 'validate_update_change' ), 10, 3 );
add_action( 'jetpack_update_plugins_change', $callable );
add_action( 'jetpack_update_themes_change', $callable );
add_action( 'jetpack_update_core_change', $callable );
add_filter( 'jetpack_sync_before_enqueue_jetpack_update_plugins_change', array(
$this,
'filter_update_keys',
), 10, 2 );
add_filter( 'jetpack_sync_before_enqueue_upgrader_process_complete', array(
$this,
'filter_upgrader_process_complete',
), 10, 2 );
add_action( 'automatic_updates_complete', $callable );
if ( is_multisite() ) {
add_filter( 'pre_update_site_option_wpmu_upgrade_site', array ( $this, 'update_core_network_event' ), 10, 2 );
add_action( 'jetpack_sync_core_update_network', $callable, 10, 3 );
}
// Send data when update completes
add_action( '_core_updated_successfully', array( $this, 'update_core' ) );
add_action( 'jetpack_sync_core_reinstalled_successfully', $callable );
add_action( 'jetpack_sync_core_autoupdated_successfully', $callable, 10, 2 );
add_action( 'jetpack_sync_core_updated_successfully', $callable, 10, 2 );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_updates', $callable );
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) );
add_filter( 'jetpack_sync_before_send_jetpack_update_themes_change', array( $this, 'expand_themes' ) );
}
public function update_core_network_event( $wp_db_version, $old_wp_db_version ) {
global $wp_version;
/**
* Sync event for when core wp network updates to a new db version
*
* @since 5.0.0
*
* @param int $wp_db_version the latest wp_db_version
* @param int $old_wp_db_version previous wp_db_version
* @param string $wp_version the latest wp_version
*
*/
do_action( 'jetpack_sync_core_update_network', $wp_db_version, $old_wp_db_version, $wp_version );
return $wp_db_version;
}
public function update_core( $new_wp_version ) {
global $pagenow;
if ( isset( $_GET[ 'action' ] ) && 'do-core-reinstall' === $_GET[ 'action' ] ) {
/**
* Sync event that fires when core reinstall was successful
*
* @since 5.0.0
*
* @param string $new_wp_version the updated WordPress version
*/
do_action( 'jetpack_sync_core_reinstalled_successfully', $new_wp_version );
return;
}
// Core was autoudpated
if ( 'update-core.php' !== $pagenow ) {
/**
* Sync event that fires when core autoupdate was successful
*
* @since 5.0.0
*
* @param string $new_wp_version the updated WordPress version
* @param string $old_wp_version the previous WordPress version
*/
do_action( 'jetpack_sync_core_autoupdated_successfully', $new_wp_version, $this->old_wp_version );
return;
}
/**
* Sync event that fires when core update was successful
*
* @since 5.0.0
*
* @param string $new_wp_version the updated WordPress version
* @param string $old_wp_version the previous WordPress version
*/
do_action( 'jetpack_sync_core_updated_successfully', $new_wp_version, $this->old_wp_version );
return;
}
public function get_update_checksum( $update, $transient ) {
$updates = array();
$no_updated = array();
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 );
}
}
}
if ( ! empty( $update->no_update ) ) {
$no_updated = array_keys( $update->no_update );
}
if ( ! isset( $no_updated[ 'jetpack/jetpack.php' ] ) && isset( $updates[ 'jetpack/jetpack.php' ] ) ) {
return false;
}
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'] );
}
}
}
if ( ! empty( $update->checked ) ) {
$no_updated = $update->checked;
}
break;
case 'update_core':
if ( ! empty( $update->updates ) && is_array( $update->updates ) ) {
foreach ( $update->updates as $response ) {
if( ! empty( $response->response ) && $response->response === 'latest' ) {
continue;
}
if ( ! empty( $response->response ) && isset( $response->packages->full ) ) {
$updates[] = array( $response->response => $response->packages->full );
}
}
}
if ( ! empty( $update->version_checked ) ) {
$no_updated = $update->version_checked;
}
if ( empty( $updates ) ) {
return false;
}
break;
}
if ( empty( $updates ) && empty( $no_updated ) ) {
return false;
}
return $this->get_check_sum( array( $no_updated, $updates ) );
}
public function validate_update_change( $value, $expiration, $transient ) {
$new_checksum = $this->get_update_checksum( $value, $transient );
if ( false === $new_checksum ) {
return;
}
$checksums = get_option( self::UPDATES_CHECKSUM_OPTION_NAME, array() );
if ( isset( $checksums[ $transient ] ) && $checksums[ $transient ] === $new_checksum ) {
return;
}
$checksums[ $transient ] = $new_checksum;
update_option( self::UPDATES_CHECKSUM_OPTION_NAME, $checksums );
/**
* jetpack_{$transient}_change
* jetpack_update_plugins_change
* jetpack_update_themes_change
* jetpack_update_core_change
*
* @since 5.1.0
*
* @param array containing info that tells us what needs updating
*
*/
do_action( "jetpack_{$transient}_change", $value );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
/**
* Tells the client to sync all updates to the server
*
* @since 4.2.0
*
* @param boolean Whether to expand updates (should always be true)
*/
do_action( 'jetpack_full_sync_updates', true );
// The number of actions enqueued, and next module state (true == done)
return array( 1, true );
}
public function estimate_full_sync_actions( $config ) {
return 1;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_updates' );
}
public function get_all_updates() {
return array(
'core' => get_site_transient( 'update_core' ),
'plugins' => get_site_transient( 'update_plugins' ),
'themes' => get_site_transient( 'update_themes' ),
);
}
// removes unnecessary keys from synced updates data
function filter_update_keys( $args ) {
$updates = $args[0];
if ( isset( $updates->no_update ) ) {
unset( $updates->no_update );
}
return $args;
}
function filter_upgrader_process_complete( $args ) {
array_shift( $args );
return $args;
}
public function expand_updates( $args ) {
if ( $args[0] ) {
return $this->get_all_updates();
}
return $args;
}
public function expand_themes( $args ) {
if ( ! isset( $args[0], $args[0]->response ) ) {
return $args;
}
if ( ! is_array( $args[0]->response ) ) {
trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response ) . ' instead', E_USER_WARNING );
return $args;
}
foreach ( $args[0]->response as $stylesheet => &$theme_data ) {
$theme = wp_get_theme( $stylesheet );
$theme_data['name'] = $theme->name;
}
return $args;
}
public function reset_data() {
delete_option( self::UPDATES_CHECKSUM_OPTION_NAME );
}
}

View File

@@ -0,0 +1,416 @@
<?php
class Jetpack_Sync_Module_Users extends Jetpack_Sync_Module {
const MAX_INITIAL_SYNC_USERS = 100;
protected $flags = array();
function name() {
return 'users';
}
// this is here to support the backfill API
public function get_object_by_id( $object_type, $id ) {
if ( $object_type === 'user' && $user = get_user_by( 'id', intval( $id ) ) ) {
return $this->sanitize_user_and_expand( $user );
}
return false;
}
public function init_listeners( $callable ) {
// users
add_action( 'user_register', array( $this, 'user_register_handler' ) );
add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) );
add_action( 'jetpack_sync_add_user', $callable, 10, 2 );
add_action( 'jetpack_sync_add_user', array( $this, 'clear_flags' ), 11 );
add_action( 'jetpack_sync_register_user', $callable, 10, 2 );
add_action( 'jetpack_sync_register_user', array( $this, 'clear_flags' ), 11 );
add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
add_action( 'jetpack_sync_save_user', array( $this, 'clear_flags' ), 11 );
add_action( 'jetpack_sync_user_locale', $callable, 10, 2 );
add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 );
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( 'jetpack_removed_user_from_blog', $callable, 10, 2 );
// user roles
add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 );
add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 );
// user capabilities
add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 );
// user authentication
add_action( 'wp_login', $callable, 10, 2 );
add_action( 'wp_logout', $callable, 10, 0 );
add_action( 'wp_masterbar_logout', $callable, 10, 0 );
// Add on init
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_users', $callable );
}
public function init_before_send() {
add_filter( 'jetpack_sync_before_send_wp_login', array( $this, 'expand_login_username' ), 10, 1 );
add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 );
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
}
private function get_user( $user ) {
if ( is_numeric( $user ) ) {
$user = get_user_by( 'id', $user );
}
if ( $user instanceof WP_User ) {
return $user;
}
return null;
}
public function sanitize_user( $user ) {
$user = $this->get_user( $user );
// this create a new user object and stops the passing of the object by reference.
$user = unserialize( serialize( $user ) );
if ( is_object( $user ) && is_object( $user->data ) ) {
unset( $user->data->user_pass );
}
return $user;
}
public function expand_user( $user ) {
if ( ! is_object( $user ) ) {
return null;
}
$user->allowed_mime_types = get_allowed_mime_types( $user );
$user->allcaps = $this->get_real_user_capabilities( $user );
if ( function_exists( 'get_user_locale' ) ) {
// Only set the user locale if it is different from the site local
if ( get_locale() !== get_user_locale( $user->ID ) ) {
$user->locale = get_user_locale( $user->ID );
}
}
return $user;
}
public function get_real_user_capabilities( $user ) {
$user_capabilities = array();
if ( is_wp_error( $user ) ) {
return $user_capabilities;
}
foreach( Jetpack_Sync_Defaults::get_capabilities_whitelist() as $capability ) {
if ( $user_has_capabilities = user_can( $user , $capability ) ) {
$user_capabilities[ $capability ] = true;
}
}
return $user_capabilities;
}
public function sanitize_user_and_expand( $user ) {
$user = $this->get_user( $user );
$user = $this->expand_user( $user );
return $this->sanitize_user( $user );
}
public function expand_action( $args ) {
// the first argument is always the user
list( $user ) = $args;
if ( $user ) {
$args[0] = $this->sanitize_user_and_expand( $user );
return $args;
}
return false;
}
public function expand_login_username( $args ) {
list( $login, $user ) = $args;
$user = $this->sanitize_user( $user );
return array( $login, $user );
}
public function expand_logout_username( $args, $user_id ) {
$user = get_userdata( $user_id );
$user = $this->sanitize_user( $user );
$login = '';
if ( is_object( $user ) && is_object( $user->data ) ) {
$login = $user->data->user_login;
}
// if we don't have a user here lets not send anything.
if ( empty( $login ) ) {
return false;
}
return array( $login, $user );
}
public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) {
$is_multisite = is_multisite();
/**
* Fires when a user is deleted on a site
*
* @since 5.4.0
*
* @param int $deleted_user_id - ID of the deleted user
* @param int $reassigned_user_id - ID of the user the deleted user's posts is reassigned to (if any)
* @param bool $is_multisite - Whether this site is a multisite installation
*/
do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite );
}
function user_register_handler( $user_id, $old_user_data = null ) {
// ensure we only sync users who are members of the current blog
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
}
/**
* Fires when a new user is registered on a site
*
* @since 4.9.0
*
* @param object The WP_User object
*/
do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) );
}
function add_user_to_blog_handler( $user_id, $old_user_data = null ) {
// ensure we only sync users who are members of the current blog
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) {
$this->add_flags( $user_id, array( 'invitation_accepted' => true ) );
}
/**
* Fires when a user is added on a site
*
* @since 4.9.0
*
* @param object The WP_User object
*/
do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) );
}
function save_user_handler( $user_id, $old_user_data = null ) {
// ensure we only sync users who are members of the current blog
if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) {
return;
}
$user = get_user_by( 'id', $user_id );
// Older versions of WP don't pass the old_user_data in ->data
if ( isset( $old_user_data->data ) ) {
$old_user = $old_user_data->data;
} else {
$old_user = $old_user_data;
}
if ( $old_user !== null && $user->user_pass !== $old_user->user_pass ) {
$this->flags[ $user_id ]['password_changed'] = true;
}
/**
* Fires when the client needs to sync an updated user
*
* @since 4.2.0
*
* @param object The WP_User object
* @param array state - New since 5.8.0
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
}
function save_user_role_handler( $user_id, $role, $old_roles = null ) {
$this->add_flags( $user_id, array( 'role_changed' => true, 'previous_role' => $old_roles ) );
//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() ) {
return;
}
/**
* This action is documented already in this file
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
}
function get_flags( $user_id ) {
if ( isset( $this->flags[ $user_id ] ) ) {
return $this->flags[ $user_id ];
}
return array();
}
function clear_flags( $user_id ) {
if ( isset( $this->flags[ $user_id ] ) ) {
unset( $this->flags[ $user_id ] );
}
}
function add_flags( $user_id, $flags ) {
$this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) );
}
function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) {
if ( $meta_key === 'locale' ) {
$this->add_flags( $user_id, array( 'locale_changed' => true ) );
}
$user = get_user_by( 'id', $user_id );
if ( $meta_key === $user->cap_key ) {
$this->add_flags( $user_id, array( 'capabilities_changed' => true ) );
}
if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) {
return;
}
if ( isset( $this->flags[ $user_id ] ) ) {
/**
* This action is documented already in this file
*/
do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) );
}
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $wpdb->usermeta";
if ( $where_sql = $this->get_where_sql( $config ) ) {
$query .= ' WHERE ' . $where_sql;
}
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
private function get_where_sql( $config ) {
global $wpdb;
$query = "meta_key = '{$wpdb->prefix}capabilities'";
// config is a list of user IDs to sync
if ( is_array( $config ) ) {
$query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
}
return $query;
}
function get_full_sync_actions() {
return array( 'jetpack_full_sync_users' );
}
function get_initial_sync_user_config() {
global $wpdb;
$user_ids = $wpdb->get_col( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0 LIMIT " . ( self::MAX_INITIAL_SYNC_USERS + 1 ) );
if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) {
return $user_ids;
} else {
return false;
}
}
public function expand_users( $args ) {
$user_ids = $args[0];
return array_map( array( $this, 'sanitize_user_and_expand' ), get_users( array( 'include' => $user_ids ) ) );
}
public function remove_user_from_blog_handler( $user_id, $blog_id ) {
//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();
//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
/**
* Fires when a user is removed from a blog on a multisite installation
*
* @since 5.4.0
*
* @param int $user_id - ID of the removed user
* @param int $reassigned_user_id - ID of the user the removed user's posts is reassigned to (if any)
*/
do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id );
}
private function is_add_new_user_to_blog() {
return Jetpack::is_function_in_backtrace( 'add_new_user_to_blog' );
}
private function is_add_user_to_blog() {
return Jetpack::is_function_in_backtrace( 'add_user_to_blog' );
}
private function is_delete_user() {
return Jetpack::is_function_in_backtrace( array( 'wp_delete_user' , 'remove_user_from_blog' ) );
}
private function is_create_user() {
$functions = 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 Jetpack::is_function_in_backtrace( $functions );
}
private function get_reassigned_network_user_id() {
$backtrace = debug_backtrace( false );
foreach ( $backtrace as $call ) {
if (
'remove_user_from_blog' === $call['function'] &&
3 === count( $call['args'] )
) {
return $call['args'][2];
}
}
return false;
}
}

View File

@@ -0,0 +1,302 @@
<?php
require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-module.php';
class Jetpack_Sync_Module_WooCommerce extends Jetpack_Sync_Module {
private $order_item_meta_whitelist = array(
// https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20
'_product_id',
'_variation_id',
'_qty',
// Tax ones also included in below class
// https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20
'_tax_class',
'_tax_status',
'_line_subtotal',
'_line_subtotal_tax',
'_line_total',
'_line_tax',
'_line_tax_data',
// https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20
'method_id',
'cost',
'total_tax',
'taxes',
// https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20
'rate_id',
'label',
'compound',
'tax_amount',
'shipping_tax_amount',
// https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php
'discount_amount',
'discount_amount_tax',
);
private $order_item_table_name;
public function __construct() {
global $wpdb;
$this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items';
// options, constants and post meta whitelists
add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 );
add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 );
add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_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' ) );
}
function name() {
return "woocommerce";
}
public function init_listeners( $callable ) {
// orders
add_action( 'woocommerce_new_order', $callable, 10, 1 );
add_action( 'woocommerce_order_status_changed', $callable, 10, 3 );
add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
// order items
add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
add_action( 'woocommerce_update_order_item', $callable, 10, 4 );
// order item meta
$this->init_listeners_for_meta_type( 'order_item', $callable );
}
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // also sends post meta
}
public function get_full_sync_actions() {
return array( 'jetpack_full_sync_woocommerce_order_items' );
}
public function init_before_send() {
// full sync
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'expand_order_item_ids' ) );
}
public function filter_order_item( $args ) {
// Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details
$args[1] = $this->build_order_item( $args[0] );
return $args;
}
public function expand_order_item_ids( $args ) {
$order_item_ids = $args[0];
global $wpdb;
$order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
$order_items = $wpdb->get_results(
"SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
);
return array(
$order_items,
$this->get_metadata( $order_item_ids, 'order_item', $this->order_item_meta_whitelist ),
);
}
public function build_order_item( $order_item_id ) {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) );
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
global $wpdb;
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_woocommerce_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config );
$count = $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
private function get_where_sql( $config ) {
return '1=1';
}
public function add_woocommerce_options_whitelist( $list ) {
return array_merge( $list, self::$wc_options_whitelist );
}
public function add_woocommerce_constants_whitelist( $list ) {
return array_merge( $list, self::$wc_constants_whitelist );
}
public function add_woocommerce_post_meta_whitelist( $list ) {
return array_merge( $list, self::$wc_post_meta_whitelist );
}
private static $wc_options_whitelist = array(
'woocommerce_currency',
'woocommerce_db_version',
'woocommerce_weight_unit',
'woocommerce_version',
'woocommerce_unforce_ssl_checkout',
'woocommerce_tax_total_display',
'woocommerce_tax_round_at_subtotal',
'woocommerce_tax_display_shop',
'woocommerce_tax_display_cart',
'woocommerce_prices_include_tax',
'woocommerce_price_thousand_sep',
'woocommerce_price_num_decimals',
'woocommerce_price_decimal_sep',
'woocommerce_notify_low_stock',
'woocommerce_notify_low_stock_amount',
'woocommerce_notify_no_stock',
'woocommerce_notify_no_stock_amount',
'woocommerce_manage_stock',
'woocommerce_force_ssl_checkout',
'woocommerce_hide_out_of_stock_items',
'woocommerce_file_download_method',
'woocommerce_enable_signup_and_login_from_checkout',
'woocommerce_enable_shipping_calc',
'woocommerce_enable_review_rating',
'woocommerce_enable_guest_checkout',
'woocommerce_enable_coupons',
'woocommerce_enable_checkout_login_reminder',
'woocommerce_enable_ajax_add_to_cart',
'woocommerce_dimension_unit',
'woocommerce_default_country',
'woocommerce_default_customer_address',
'woocommerce_currency_pos',
'woocommerce_api_enabled',
'woocommerce_allow_tracking',
);
private static $wc_constants_whitelist = array(
//woocommerce options
'WC_PLUGIN_FILE',
'WC_ABSPATH',
'WC_PLUGIN_BASENAME',
'WC_VERSION',
'WOOCOMMERCE_VERSION',
'WC_ROUNDING_PRECISION',
'WC_DISCOUNT_ROUNDING_MODE',
'WC_TAX_ROUNDING_MODE',
'WC_DELIMITER',
'WC_LOG_DIR',
'WC_SESSION_CACHE_GROUP',
'WC_TEMPLATE_DEBUG_MODE',
);
private static $wc_post_meta_whitelist = array(
//woocommerce products
// https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21
'_visibility',
'_sku',
'_price',
'_regular_price',
'_sale_price',
'_sale_price_dates_from',
'_sale_price_dates_to',
'total_sales',
'_tax_status',
'_tax_class',
'_manage_stock',
'_backorders',
'_sold_individually',
'_weight',
'_length',
'_width',
'_height',
'_upsell_ids',
'_crosssell_ids',
'_purchase_note',
'_default_attributes',
'_product_attributes',
'_virtual',
'_downloadable',
'_download_limit',
'_download_expiry',
'_featured',
'_downloadable_files',
'_wc_rating_count',
'_wc_average_rating',
'_wc_review_count',
'_variation_description',
'_thumbnail_id',
'_file_paths',
'_product_image_gallery',
'_product_version',
'_wp_old_slug',
//woocommerce orders
// https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27
'_order_key',
'_order_currency',
// '_billing_first_name', do not sync these as they contain personal data
// '_billing_last_name',
// '_billing_company',
// '_billing_address_1',
// '_billing_address_2',
'_billing_city',
'_billing_state',
'_billing_postcode',
'_billing_country',
// '_billing_email', do not sync these as they contain personal data
// '_billing_phone',
// '_shipping_first_name',
// '_shipping_last_name',
// '_shipping_company',
// '_shipping_address_1',
// '_shipping_address_2',
'_shipping_city',
'_shipping_state',
'_shipping_postcode',
'_shipping_country',
'_completed_date',
'_paid_date',
'_cart_discount',
'_cart_discount_tax',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_payment_method',
'_payment_method_title',
// '_transaction_id', do not sync these as they contain personal data
// '_customer_ip_address',
// '_customer_user_agent',
'_created_via',
'_order_version',
'_prices_include_tax',
'_date_completed',
'_date_paid',
'_payment_tokens',
'_billing_address_index',
'_shipping_address_index',
'_recorded_sales',
'_recorded_coupon_usage_counts',
// https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539
'_download_permissions_granted',
// https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594
'_order_stock_reduced',
//woocommerce order refunds
// https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20
'_order_currency',
'_refund_amount',
'_refunded_by',
'_refund_reason',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_order_version',
'_prices_include_tax',
'_payment_tokens',
);
}

View File

@@ -0,0 +1,79 @@
<?php
class Jetpack_Sync_Module_WP_Super_Cache extends Jetpack_Sync_Module {
public function __construct() {
add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_wp_super_cache_constants_whitelist' ), 10 );
add_filter( 'jetpack_sync_callable_whitelist', array( $this, 'add_wp_super_cache_callable_whitelist' ), 10 );
}
static $wp_super_cache_constants = array(
'WPLOCKDOWN',
'WPSC_DISABLE_COMPRESSION',
'WPSC_DISABLE_LOCKING',
'WPSC_DISABLE_HTACCESS_UPDATE',
'ADVANCEDCACHEPROBLEM',
);
static $wp_super_cache_callables = array(
'wp_super_cache_globals' => array( 'Jetpack_Sync_Module_WP_Super_Cache', 'get_wp_super_cache_globals' ),
);
public function name() {
return 'wp-super-cache';
}
public static function get_wp_super_cache_globals() {
global $wp_cache_mod_rewrite;
global $cache_enabled;
global $super_cache_enabled;
global $ossdlcdn;
global $cache_rebuild_files;
global $wp_cache_mobile;
global $wp_super_cache_late_init;
global $wp_cache_anon_only;
global $wp_cache_not_logged_in;
global $wp_cache_clear_on_post_edit;
global $wp_cache_mobile_enabled;
global $wp_super_cache_debug;
global $cache_max_time;
global $wp_cache_refresh_single_only;
global $wp_cache_mfunc_enabled;
global $wp_supercache_304;
global $wp_cache_no_cache_for_get;
global $wp_cache_mutex_disabled;
global $cache_jetpack;
global $cache_domain_mapping;
return array(
'wp_cache_mod_rewrite' => $wp_cache_mod_rewrite,
'cache_enabled' => $cache_enabled,
'super_cache_enabled' => $super_cache_enabled,
'ossdlcdn' => $ossdlcdn,
'cache_rebuild_files' => $cache_rebuild_files,
'wp_cache_mobile' => $wp_cache_mobile,
'wp_super_cache_late_init' => $wp_super_cache_late_init,
'wp_cache_anon_only' => $wp_cache_anon_only,
'wp_cache_not_logged_in' => $wp_cache_not_logged_in,
'wp_cache_clear_on_post_edit' => $wp_cache_clear_on_post_edit,
'wp_cache_mobile_enabled' => $wp_cache_mobile_enabled,
'wp_super_cache_debug' => $wp_super_cache_debug,
'cache_max_time' => $cache_max_time,
'wp_cache_refresh_single_only' => $wp_cache_refresh_single_only,
'wp_cache_mfunc_enabled' => $wp_cache_mfunc_enabled,
'wp_supercache_304' => $wp_supercache_304,
'wp_cache_no_cache_for_get' => $wp_cache_no_cache_for_get,
'wp_cache_mutex_disabled' => $wp_cache_mutex_disabled,
'cache_jetpack' => $cache_jetpack,
'cache_domain_mapping' => $cache_domain_mapping,
);
}
public function add_wp_super_cache_constants_whitelist( $list ) {
return array_merge( $list, self::$wp_super_cache_constants );
}
public function add_wp_super_cache_callable_whitelist( $list ) {
return array_merge( $list, self::$wp_super_cache_callables );
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* Basic methods implemented by Jetpack Sync extensions
*/
abstract class Jetpack_Sync_Module {
const ARRAY_CHUNK_SIZE = 10;
abstract public function name();
public function get_object_by_id( $object_type, $id ) {
return false;
}
// override these to set up listeners and set/reset data/defaults
public function init_listeners( $callable ) {
}
public function init_full_sync_listeners( $callable ) {
}
public function init_before_send() {
}
public function set_defaults() {
}
public function reset_data() {
}
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
// in subclasses, return the number of actions enqueued, and next module state (true == done)
return array( 0, true );
}
public function estimate_full_sync_actions( $config ) {
// in subclasses, return the number of items yet to be enqueued
return 0;
}
public function get_full_sync_actions() {
return array();
}
protected function count_actions( $action_names, $actions_to_count ) {
return count( array_intersect( $action_names, $actions_to_count ) );
}
protected function get_check_sum( $values ) {
return crc32( wp_json_encode( jetpack_json_wrap( $values ) ) );
}
protected function still_valid_checksum( $sums_to_check, $name, $new_sum ) {
if ( isset( $sums_to_check[ $name ] ) && $sums_to_check[ $name ] === $new_sum ) {
return true;
}
return false;
}
protected function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql, $max_items_to_enqueue, $state ) {
global $wpdb;
if ( ! $where_sql ) {
$where_sql = '1 = 1';
}
$items_per_page = 1000;
$page = 1;
$chunk_count = 0;
$previous_max_id = $state ? $state : '~0';
$listener = Jetpack_Sync_Listener::get_instance();
// count down from max_id to min_id so we get newest posts/comments/etc first
while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} < {$previous_max_id} ORDER BY {$id_field} DESC LIMIT {$items_per_page}" ) ) {
// Request posts in groups of N for efficiency
$chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE );
// if we hit our row limit, process and return
if ( $chunk_count + count( $chunked_ids ) >= $max_items_to_enqueue ) {
$remaining_items_count = $max_items_to_enqueue - $chunk_count;
$remaining_items = array_slice( $chunked_ids, 0, $remaining_items_count );
$listener->bulk_enqueue_full_sync_actions( $action_name, $remaining_items );
$last_chunk = end( $remaining_items );
return array( $remaining_items_count + $chunk_count, end( $last_chunk ) );
}
$listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids );
$chunk_count += count( $chunked_ids );
$page += 1;
$previous_max_id = end( $ids );
}
return array( $chunk_count, true );
}
protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) {
global $wpdb;
$table = _get_meta_table( $meta_type );
$id = $meta_type . '_id';
if ( ! $table ) {
return array();
}
$private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'";
return array_map(
array( $this, 'unserialize_meta' ),
$wpdb->get_results(
"SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )'.
" AND meta_key IN ( $private_meta_whitelist_sql ) "
, OBJECT )
);
}
public function init_listeners_for_meta_type( $meta_type, $callable ) {
add_action( "added_{$meta_type}_meta", $callable, 10, 4 );
add_action( "updated_{$meta_type}_meta", $callable, 10, 4 );
add_action( "deleted_{$meta_type}_meta", $callable, 10, 4 );
}
public function init_meta_whitelist_handler( $meta_type, $whitelist_handler ) {
add_filter( "jetpack_sync_before_enqueue_added_{$meta_type}_meta", $whitelist_handler );
add_filter( "jetpack_sync_before_enqueue_updated_{$meta_type}_meta", $whitelist_handler );
add_filter( "jetpack_sync_before_enqueue_deleted_{$meta_type}_meta", $whitelist_handler );
}
protected function get_term_relationships( $ids ) {
global $wpdb;
return $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )', OBJECT );
}
public function unserialize_meta( $meta ) {
$meta->meta_value = maybe_unserialize( $meta->meta_value );
return $meta;
}
public function get_objects_by_id( $object_type, $ids ) {
if ( empty( $ids ) || empty( $object_type ) ) {
return array();
}
$objects = array();
foreach( (array) $ids as $id ) {
$object = $this->get_object_by_id( $object_type, $id );
// Only add object if we have the object.
if ( $object ) {
$objects[ $id ] = $object;
}
}
return $objects;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* simple wrapper that allows enumerating cached static instances
* of sync modules
*/
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-posts.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-import.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-comments.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-constants.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-callables.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-options.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-network-options.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-updates.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-users.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-themes.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-menus.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-attachments.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-meta.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-terms.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-plugins.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-full-sync.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-stats.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-protect.php';
class Jetpack_Sync_Modules {
private static $default_sync_modules = array(
'Jetpack_Sync_Module_Constants',
'Jetpack_Sync_Module_Callables',
'Jetpack_Sync_Module_Options',
'Jetpack_Sync_Module_Network_Options',
'Jetpack_Sync_Module_Terms',
'Jetpack_Sync_Module_Themes',
'Jetpack_Sync_Module_Menus',
'Jetpack_Sync_Module_Users',
'Jetpack_Sync_Module_Posts',
'Jetpack_Sync_Module_Import',
'Jetpack_Sync_Module_Protect',
'Jetpack_Sync_Module_Comments',
'Jetpack_Sync_Module_Updates',
'Jetpack_Sync_Module_Attachments',
'Jetpack_Sync_Module_Meta',
'Jetpack_Sync_Module_Plugins',
'Jetpack_Sync_Module_Full_Sync',
'Jetpack_Sync_Module_Stats',
);
private static $initialized_modules = null;
public static function get_modules() {
if ( null === self::$initialized_modules ) {
self::$initialized_modules = self::initialize_modules();
}
return self::$initialized_modules;
}
public static function set_defaults() {
foreach ( self::get_modules() as $module ) {
$module->set_defaults();
}
}
public static function get_module( $module_name ) {
foreach ( self::get_modules() as $module ) {
if ( $module->name() === $module_name ) {
return $module;
}
}
return false;
}
static function initialize_modules() {
/**
* Filters the list of class names of sync modules.
* If you add to this list, make sure any classes implement the
* Jetpack_Sync_Module interface.
*
* @since 4.2.0
*/
$modules = apply_filters( 'jetpack_sync_modules', self::$default_sync_modules );
$modules = array_map( array( 'Jetpack_Sync_Modules', 'load_module' ), $modules );
return array_map( array( 'Jetpack_Sync_Modules', 'set_module_defaults' ), $modules );
}
static function load_module( $module_name ) {
return new $module_name;
}
static function set_module_defaults( $module ) {
$module->set_defaults();
if ( method_exists( $module, 'set_late_default' ) ) {
add_action( 'init', array( $module, 'set_late_default' ), 90 );
}
return $module;
}
}

View File

@@ -0,0 +1,455 @@
<?php
/**
* A buffer of items from the queue that can be checked out
*/
class Jetpack_Sync_Queue_Buffer {
public $id;
public $items_with_ids;
public function __construct( $id, $items_with_ids ) {
$this->id = $id;
$this->items_with_ids = $items_with_ids;
}
public function get_items() {
return array_combine( $this->get_item_ids(), $this->get_item_values() );
}
public function get_item_values() {
return Jetpack_Sync_Utils::get_item_values( $this->items_with_ids );
}
public function get_item_ids() {
return Jetpack_Sync_Utils::get_item_ids( $this->items_with_ids );
}
}
/**
* A persistent queue that can be flushed in increments of N items,
* and which blocks reads until checked-out buffers are checked in or
* closed. This uses raw SQL for two reasons: speed, and not triggering
* tons of added_option callbacks.
*/
class Jetpack_Sync_Queue {
public $id;
private $row_iterator;
function __construct( $id ) {
$this->id = str_replace( '-', '_', $id ); // necessary to ensure we don't have ID collisions in the SQL
$this->row_iterator = 0;
$this->random_int = mt_rand( 1, 1000000 );
}
function add( $item ) {
global $wpdb;
$added = false;
// this basically tries to add the option until enough time has elapsed that
// it has a unique (microtime-based) option key
while ( ! $added ) {
$rows_added = $wpdb->query( $wpdb->prepare(
"INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)",
$this->get_next_data_row_option_name(),
serialize( $item ),
'no'
) );
$added = ( 0 !== $rows_added );
}
}
// Attempts to insert all the items in a single SQL query. May be subject to query size limits!
function add_all( $items ) {
global $wpdb;
$base_option_name = $this->get_next_data_row_option_name();
$query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES ";
$rows = array();
for ( $i = 0; $i < count( $items ); $i += 1 ) {
$option_name = esc_sql( $base_option_name . '-' . $i );
$option_value = esc_sql( serialize( $items[ $i ] ) );
$rows[] = "('$option_name', '$option_value', 'no')";
}
$rows_added = $wpdb->query( $query . join( ',', $rows ) );
if ( count( $items ) === $rows_added ) {
return new WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" );
}
}
// Peek at the front-most item on the queue without checking it out
function peek( $count = 1 ) {
$items = $this->fetch_items( $count );
if ( $items ) {
return Jetpack_Sync_Utils::get_item_values( $items );
}
return array();
}
// lag is the difference in time between the age of the oldest item
// (aka first or frontmost item) and the current time
function lag() {
global $wpdb;
$first_item_name = $wpdb->get_var( $wpdb->prepare(
"SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1",
"jpsq_{$this->id}-%"
) );
if ( ! $first_item_name ) {
return 0;
}
// break apart the item name to get the timestamp
$matches = null;
if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) {
return microtime( true ) - floatval( $matches[1] );
} else {
return 0;
}
}
function reset() {
global $wpdb;
$this->delete_checkout_id();
$wpdb->query( $wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s", "jpsq_{$this->id}-%"
) );
}
function size() {
global $wpdb;
return (int) $wpdb->get_var( $wpdb->prepare(
"SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s", "jpsq_{$this->id}-%"
) );
}
// we use this peculiar implementation because it's much faster than count(*)
function has_any_items() {
global $wpdb;
$value = $wpdb->get_var( $wpdb->prepare(
"SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )", "jpsq_{$this->id}-%"
) );
return ( $value === '1' );
}
function checkout( $buffer_size ) {
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
$buffer_id = uniqid();
$result = $this->set_checkout_id( $buffer_id );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
$items = $this->fetch_items( $buffer_size );
if ( count( $items ) === 0 ) {
return false;
}
$buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) );
return $buffer;
}
// this checks out rows until it either empties the queue or hits a certain memory limit
// it loads the sizes from the DB first so that it doesn't accidentally
// load more data into memory than it needs to.
// The only way it will load more items than $max_size is if a single queue item
// exceeds the memory limit, but in that case it will send that item by itself.
function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) {
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
$buffer_id = uniqid();
$result = $this->set_checkout_id( $buffer_id );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
// get the map of buffer_id -> memory_size
global $wpdb;
$items_with_size = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d",
"jpsq_{$this->id}-%",
$max_buffer_size
),
OBJECT
);
if ( count( $items_with_size ) === 0 ) {
return false;
}
$total_memory = 0;
$min_item_id = $max_item_id = $items_with_size[0]->id;
foreach ( $items_with_size as $id => $item_with_size ) {
$total_memory += $item_with_size->value_size;
// if this is the first item and it exceeds memory, allow loop to continue
// we will exit on the next iteration instead
if ( $total_memory > $max_memory && $id > 0 ) {
break;
}
$max_item_id = $item_with_size->id;
}
$query = $wpdb->prepare(
"SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC",
$min_item_id,
$max_item_id
);
$items = $wpdb->get_results( $query, OBJECT );
foreach ( $items as $item ) {
$item->value = maybe_unserialize( $item->value );
}
if ( count( $items ) === 0 ) {
$this->delete_checkout_id();
return false;
}
$buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, $items );
return $buffer;
}
function checkin( $buffer ) {
$is_valid = $this->validate_checkout( $buffer );
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
$this->delete_checkout_id();
return true;
}
function close( $buffer, $ids_to_remove = null ) {
$is_valid = $this->validate_checkout( $buffer );
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
$this->delete_checkout_id();
// by default clear all items in the buffer
if ( is_null( $ids_to_remove ) ) {
$ids_to_remove = $buffer->get_item_ids();
}
global $wpdb;
if ( count( $ids_to_remove ) > 0 ) {
$sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids_to_remove ), '%s' ) ) . ')';
$query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids_to_remove ) );
$wpdb->query( $query );
}
return true;
}
function flush_all() {
$items = Jetpack_Sync_Utils::get_item_values( $this->fetch_items() );
$this->reset();
return $items;
}
function get_all() {
return $this->fetch_items();
}
// use with caution, this could allow multiple processes to delete
// and send from the queue at the same time
function force_checkin() {
$this->delete_checkout_id();
}
// used to lock checkouts from the queue.
// tries to wait up to $timeout seconds for the queue to be empty
function lock( $timeout = 30 ) {
$tries = 0;
while ( $this->has_any_items() && $tries < $timeout ) {
sleep( 1 );
$tries += 1;
}
if ( $tries === 30 ) {
return new WP_Error( 'lock_timeout', 'Timeout waiting for sync queue to empty' );
}
if ( $this->get_checkout_id() ) {
return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' );
}
// hopefully this means we can acquire a checkout?
$result = $this->set_checkout_id( 'lock' );
if ( ! $result || is_wp_error( $result ) ) {
return $result;
}
return true;
}
function unlock() {
return $this->delete_checkout_id();
}
private function get_checkout_id() {
global $wpdb;
$checkout_value = $wpdb->get_var(
$wpdb->prepare(
"SELECT option_value FROM $wpdb->options WHERE option_name = %s",
$this->get_lock_option_name()
)
);
if ( $checkout_value ) {
list( $checkout_id, $timestamp ) = explode( ':', $checkout_value );
if ( intval( $timestamp ) > time() ) {
return $checkout_id;
}
}
return false;
}
private function set_checkout_id( $checkout_id ) {
global $wpdb;
$expires = time() + Jetpack_Sync_Defaults::$default_sync_queue_lock_timeout;
$updated_num = $wpdb->query(
$wpdb->prepare(
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
"$checkout_id:$expires",
$this->get_lock_option_name()
)
);
if ( ! $updated_num ) {
$updated_num = $wpdb->query(
$wpdb->prepare(
"INSERT INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
$this->get_lock_option_name(),
"$checkout_id:$expires"
)
);
}
return $updated_num;
}
private function delete_checkout_id() {
global $wpdb;
// rather than delete, which causes fragmentation, we update in place
return $wpdb->query(
$wpdb->prepare(
"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
"0:0",
$this->get_lock_option_name()
)
);
}
private function get_lock_option_name() {
return "jpsq_{$this->id}_checkout";
}
private function get_next_data_row_option_name() {
// this option is specifically chosen to, as much as possible, preserve time order
// and minimise the possibility of collisions between multiple processes working
// at the same time
// TODO: confirm we only need to support PHP 5.05+ (otherwise we'll need to emulate microtime as float, and avoid PHP_INT_MAX)
// @see: http://php.net/manual/en/function.microtime.php
$timestamp = sprintf( '%.6f', microtime( true ) );
// row iterator is used to avoid collisions where we're writing data waaay fast in a single process
if ( $this->row_iterator === PHP_INT_MAX ) {
$this->row_iterator = 0;
} else {
$this->row_iterator += 1;
}
return 'jpsq_' . $this->id . '-' . $timestamp . '-' . $this->random_int . '-' . $this->row_iterator;
}
private function fetch_items( $limit = null ) {
global $wpdb;
if ( $limit ) {
$query_sql = $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->id}-%", $limit );
} else {
$query_sql = $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->id}-%" );
}
$items = $wpdb->get_results( $query_sql, OBJECT );
foreach ( $items as $item ) {
$item->value = maybe_unserialize( $item->value );
}
return $items;
}
private function validate_checkout( $buffer ) {
if ( ! $buffer instanceof Jetpack_Sync_Queue_Buffer ) {
return new WP_Error( 'not_a_buffer', 'You must checkin an instance of Jetpack_Sync_Queue_Buffer' );
}
$checkout_id = $this->get_checkout_id();
if ( ! $checkout_id ) {
return new WP_Error( 'buffer_not_checked_out', 'There are no checked out buffers' );
}
if ( $checkout_id != $buffer->id ) {
return new WP_Error( 'buffer_mismatch', 'The buffer you checked in was not checked out' );
}
return true;
}
}
class Jetpack_Sync_Utils {
static function get_item_values( $items ) {
return array_map( array( __CLASS__, 'get_item_value' ), $items );
}
static function get_item_ids( $items ) {
return array_map( array( __CLASS__, 'get_item_id' ), $items );
}
static private function get_item_value( $item ) {
return $item->value;
}
static private function get_item_id( $item ) {
return $item->id;
}
}

View File

@@ -0,0 +1,406 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-queue.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-array-codec.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-simple-codec.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
/**
* This class grabs pending actions from the queue and sends them
*/
class Jetpack_Sync_Sender {
const NEXT_SYNC_TIME_OPTION_NAME = 'jetpack_next_sync_time';
const WPCOM_ERROR_SYNC_DELAY = 60;
const QUEUE_LOCKED_SYNC_DELAY = 10;
private $dequeue_max_bytes;
private $upload_max_bytes;
private $upload_max_rows;
private $max_dequeue_time;
private $sync_wait_time;
private $sync_wait_threshold;
private $enqueue_wait_time;
private $sync_queue;
private $full_sync_queue;
private $codec;
private $old_user;
// singleton functions
private static $instance;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
// this is necessary because you can't use "new" when you declare instance properties >:(
protected function __construct() {
$this->set_defaults();
$this->init();
}
private function init() {
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_set_user_from_token' ), 1 );
add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_clear_user_from_token' ), 20 );
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module->init_before_send();
}
}
public function maybe_set_user_from_token( ) {
$jetpack = Jetpack::init();
$verified_user = $jetpack->verify_xml_rpc_signature();
if ( Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) &&
! is_wp_error( $verified_user )
&& $verified_user
) {
$old_user = wp_get_current_user();
$this->old_user = isset( $old_user->ID ) ? $old_user->ID : 0;
wp_set_current_user( $verified_user['user_id'] );
}
}
public function maybe_clear_user_from_token() {
if ( isset( $this->old_user ) ) {
wp_set_current_user( $this->old_user );
}
}
public function get_next_sync_time( $queue_name ) {
return (double) get_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, 0 );
}
public function set_next_sync_time( $time, $queue_name ) {
return update_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, $time, true );
}
public function do_full_sync() {
if ( ! Jetpack_Sync_Modules::get_module( 'full-sync' ) ) {
return;
}
$this->continue_full_sync_enqueue();
return $this->do_sync_and_set_delays( $this->full_sync_queue );
}
private function continue_full_sync_enqueue() {
if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return false;
}
if ( $this->get_next_sync_time( 'full-sync-enqueue' ) > microtime( true ) ) {
return false;
}
Jetpack_Sync_Modules::get_module( 'full-sync' )->continue_enqueuing();
$this->set_next_sync_time( time() + $this->get_enqueue_wait_time(), 'full-sync-enqueue' );
}
public function do_sync() {
return $this->do_sync_and_set_delays( $this->sync_queue );
}
public function do_sync_and_set_delays( $queue ) {
// don't sync if importing
if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
return new WP_Error( 'is_importing' );
}
// don't sync if we are throttled
if ( $this->get_next_sync_time( $queue->id ) > microtime( true ) ) {
return new WP_Error( 'sync_throttled' );
}
$start_time = microtime( true );
Jetpack_Sync_Settings::set_is_syncing( true );
$sync_result = $this->do_sync_for_queue( $queue );
Jetpack_Sync_Settings::set_is_syncing( false );
$exceeded_sync_wait_threshold = ( microtime( true ) - $start_time ) > (double) $this->get_sync_wait_threshold();
if ( is_wp_error( $sync_result ) ) {
if ( 'unclosed_buffer' === $sync_result->get_error_code() ) {
$this->set_next_sync_time( time() + self::QUEUE_LOCKED_SYNC_DELAY, $queue->id );
}
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
$this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id );
}
return $sync_result;
}
public function get_items_to_send( $buffer, $encode = true ) {
// track how long we've been processing so we can avoid request timeouts
$start_time = microtime( true );
$upload_size = 0;
$items_to_send = array();
$items = $buffer->get_items();
// set up current screen to avoid errors rendering content
require_once( ABSPATH . 'wp-admin/includes/class-wp-screen.php' );
require_once( ABSPATH . 'wp-admin/includes/screen.php' );
set_current_screen( 'sync' );
$skipped_items_ids = array();
// we estimate the total encoded size as we go by encoding each item individually
// this is expensive, but the only way to really know :/
foreach ( $items as $key => $item ) {
// Suspending cache addition help prevent overloading in memory cache of large sites.
wp_suspend_cache_addition( true );
/**
* Modify the data within an action before it is serialized and sent to the server
* For example, during full sync this expands Post ID's into full Post objects,
* so that we don't have to serialize the whole object into the queue.
*
* @since 4.2.0
*
* @param array The action parameters
* @param int The ID of the user who triggered the action
*/
$item[1] = apply_filters( 'jetpack_sync_before_send_' . $item[0], $item[1], $item[2] );
wp_suspend_cache_addition( false );
if ( $item[1] === false ) {
$skipped_items_ids[] = $key;
continue;
}
$encoded_item = $encode ? $this->codec->encode( $item ) : $item;
$upload_size += strlen( $encoded_item );
if ( $upload_size > $this->upload_max_bytes && count( $items_to_send ) > 0 ) {
break;
}
$items_to_send[ $key ] = $encoded_item;
if ( microtime(true) - $start_time > $this->max_dequeue_time ) {
break;
}
}
return array( $items_to_send, $skipped_items_ids, $items, microtime( true ) - $start_time );
}
public function do_sync_for_queue( $queue ) {
do_action( 'jetpack_sync_before_send_queue_' . $queue->id );
if ( $queue->size() === 0 ) {
return new WP_Error( 'empty_queue_' . $queue->id );
}
// now that we're sure we are about to sync, try to
// ignore user abort so we can avoid getting into a
// bad state
if ( function_exists( 'ignore_user_abort' ) ) {
ignore_user_abort( true );
}
$checkout_start_time = microtime( true );
$buffer = $queue->checkout_with_memory_limit( $this->dequeue_max_bytes, $this->upload_max_rows );
if ( ! $buffer ) {
// buffer has no items
return new WP_Error( 'empty_buffer' );
}
if ( is_wp_error( $buffer ) ) {
return $buffer;
}
$checkout_duration = microtime( true ) - $checkout_start_time;
list( $items_to_send, $skipped_items_ids, $items, $preprocess_duration ) = $this->get_items_to_send( $buffer, true );
if ( ! empty( $items_to_send ) ) {
/**
* Fires when data is ready to send to the server.
* Return false or WP_Error to abort the sync (e.g. if there's an error)
* The items will be automatically re-sent later
*
* @since 4.2.0
*
* @param array $data The action buffer
* @param string $codec The codec name used to encode the data
* @param double $time The current time
* @param string $queue The queue used to send ('sync' or 'full_sync')
*/
Jetpack_Sync_Settings::set_is_sending( true );
$processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->codec->name(), microtime( true ), $queue->id, $checkout_duration, $preprocess_duration );
Jetpack_Sync_Settings::set_is_sending( false );
} else {
$processed_item_ids = $skipped_items_ids;
$skipped_items_ids = array();
}
if ( ! $processed_item_ids || is_wp_error( $processed_item_ids ) ) {
$checked_in_item_ids = $queue->checkin( $buffer );
if ( is_wp_error( $checked_in_item_ids ) ) {
error_log( 'Error checking in buffer: ' . $checked_in_item_ids->get_error_message() );
$queue->force_checkin();
}
if ( is_wp_error( $processed_item_ids ) ) {
return new WP_Error( 'wpcom_error', $processed_item_ids->get_error_code() );
}
// returning a WP_Error('wpcom_error') is a sign to the caller that we should wait a while
// before syncing again
return new WP_Error( 'wpcom_error', 'jetpack_sync_send_data_false' );
} else {
// detect if the last item ID was an error
$had_wp_error = is_wp_error( end( $processed_item_ids ) );
if ( $had_wp_error ) {
$wp_error = array_pop( $processed_item_ids );
}
// also checkin any items that were skipped
if ( count( $skipped_items_ids ) > 0 ) {
$processed_item_ids = array_merge( $processed_item_ids, $skipped_items_ids );
}
$processed_items = array_intersect_key( $items, array_flip( $processed_item_ids ) );
/**
* Allows us to keep track of all the actions that have been sent.
* Allows us to calculate the progress of specific actions.
*
* @since 4.2.0
*
* @param array $processed_actions The actions that we send successfully.
*/
do_action( 'jetpack_sync_processed_actions', $processed_items );
$queue->close( $buffer, $processed_item_ids );
// returning a WP_Error is a sign to the caller that we should wait a while
// before syncing again
if ( $had_wp_error ) {
return new WP_Error( 'wpcom_error', $wp_error->get_error_code() );
}
}
return true;
}
function get_sync_queue() {
return $this->sync_queue;
}
function get_full_sync_queue() {
return $this->full_sync_queue;
}
function get_codec() {
return $this->codec;
}
function set_codec() {
if ( function_exists( 'gzinflate' ) ) {
$this->codec = new Jetpack_Sync_JSON_Deflate_Array_Codec();
} else {
$this->codec = new Jetpack_Sync_Simple_Codec();
}
}
function send_checksum() {
require_once 'class.jetpack-sync-wp-replicastore.php';
$store = new Jetpack_Sync_WP_Replicastore();
do_action( 'jetpack_sync_checksum', $store->checksum_all() );
}
function reset_sync_queue() {
$this->sync_queue->reset();
}
function reset_full_sync_queue() {
$this->full_sync_queue->reset();
}
function set_dequeue_max_bytes( $size ) {
$this->dequeue_max_bytes = $size;
}
// in bytes
function set_upload_max_bytes( $max_bytes ) {
$this->upload_max_bytes = $max_bytes;
}
// in rows
function set_upload_max_rows( $max_rows ) {
$this->upload_max_rows = $max_rows;
}
// in seconds
function set_sync_wait_time( $seconds ) {
$this->sync_wait_time = $seconds;
}
function get_sync_wait_time() {
return $this->sync_wait_time;
}
function set_enqueue_wait_time( $seconds ) {
$this->enqueue_wait_time = $seconds;
}
function get_enqueue_wait_time() {
return $this->enqueue_wait_time;
}
// in seconds
function set_sync_wait_threshold( $seconds ) {
$this->sync_wait_threshold = $seconds;
}
function get_sync_wait_threshold() {
return $this->sync_wait_threshold;
}
// in seconds
function set_max_dequeue_time( $seconds ) {
$this->max_dequeue_time = $seconds;
}
function set_defaults() {
$this->sync_queue = new Jetpack_Sync_Queue( 'sync' );
$this->full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' );
$this->set_codec();
// saved settings
Jetpack_Sync_Settings::set_importing( null );
$settings = Jetpack_Sync_Settings::get_settings();
$this->set_dequeue_max_bytes( $settings['dequeue_max_bytes'] );
$this->set_upload_max_bytes( $settings['upload_max_bytes'] );
$this->set_upload_max_rows( $settings['upload_max_rows'] );
$this->set_sync_wait_time( $settings['sync_wait_time'] );
$this->set_enqueue_wait_time( $settings['enqueue_wait_time'] );
$this->set_sync_wait_threshold( $settings['sync_wait_threshold'] );
$this->set_max_dequeue_time( Jetpack_Sync_Defaults::get_max_sync_execution_time() );
}
function reset_data() {
$this->reset_sync_queue();
$this->reset_full_sync_queue();
foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
$module->reset_data();
}
foreach ( array( 'sync', 'full_sync', 'full-sync-enqueue' ) as $queue_name ) {
delete_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name );
}
Jetpack_Sync_Settings::reset_data();
}
function uninstall() {
// Lets delete all the other fun stuff like transient and option and the sync queue
$this->reset_data();
// delete the full sync status
delete_option( 'jetpack_full_sync_status' );
// clear the sync cron.
wp_clear_scheduled_hook( 'jetpack_sync_cron' );
wp_clear_scheduled_hook( 'jetpack_sync_full_cron' );
}
}

View File

@@ -0,0 +1,106 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-array-codec.php';
/**
* Simple version of a Jetpack Sync Server - just receives arrays of events and
* issues them locally with the 'jetpack_sync_remote_action' action.
*/
class Jetpack_Sync_Server {
private $codec;
const MAX_TIME_PER_REQUEST_IN_SECONDS = 15;
const BLOG_LOCK_TRANSIENT_PREFIX = 'jp_sync_req_lock_';
const BLOG_LOCK_TRANSIENT_EXPIRY = 60; // seconds
// this is necessary because you can't use "new" when you declare instance properties >:(
function __construct() {
$this->codec = new Jetpack_Sync_JSON_Deflate_Array_Codec();
}
function set_codec( iJetpack_Sync_Codec $codec ) {
$this->codec = $codec;
}
function attempt_request_lock( $blog_id, $expiry = self::BLOG_LOCK_TRANSIENT_EXPIRY ) {
$transient_name = $this->get_concurrent_request_transient_name( $blog_id );
$locked_time = get_site_transient( $transient_name );
if ( $locked_time ) {
return false;
}
set_site_transient( $transient_name, microtime( true ), $expiry );
return true;
}
private function get_concurrent_request_transient_name( $blog_id ) {
return self::BLOG_LOCK_TRANSIENT_PREFIX . $blog_id;
}
function remove_request_lock( $blog_id ) {
delete_site_transient( $this->get_concurrent_request_transient_name( $blog_id ) );
}
function receive( $data, $token = null, $sent_timestamp = null, $queue_id = null ) {
$start_time = microtime( true );
if ( ! is_array( $data ) ) {
return new WP_Error( 'action_decoder_error', 'Events must be an array' );
}
if ( $token && ! $this->attempt_request_lock( $token->blog_id ) ) {
/**
* Fires when the server receives two concurrent requests from the same blog
*
* @since 4.2.0
*
* @param token The token object of the misbehaving site
*/
do_action( 'jetpack_sync_multi_request_fail', $token );
return new WP_Error( 'concurrent_request_error', 'There is another request running for the same blog ID' );
}
$events = wp_unslash( array_map( array( $this->codec, 'decode' ), $data ) );
$events_processed = array();
/**
* Fires when an array of actions are received from a remote Jetpack site
*
* @since 4.2.0
*
* @param array Array of actions received from the remote site
*/
do_action( 'jetpack_sync_remote_actions', $events, $token );
foreach ( $events as $key => $event ) {
list( $action_name, $args, $user_id, $timestamp, $silent ) = $event;
/**
* Fires when an action is received from a remote Jetpack site
*
* @since 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
*/
do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token );
$events_processed[] = $key;
if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) {
break;
}
}
if ( $token ) {
$this->remove_request_lock( $token->blog_id );
}
return $events_processed;
}
}

View File

@@ -0,0 +1,179 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
class Jetpack_Sync_Settings {
const SETTINGS_OPTION_PREFIX = 'jetpack_sync_settings_';
static $valid_settings = array(
'dequeue_max_bytes' => true,
'upload_max_bytes' => true,
'upload_max_rows' => true,
'sync_wait_time' => true,
'sync_wait_threshold' => true,
'enqueue_wait_time' => true,
'max_queue_size' => true,
'max_queue_lag' => true,
'queue_max_writes_sec' => true,
'post_types_blacklist' => true,
'disable' => true,
'render_filtered_content' => true,
'post_meta_whitelist' => true,
'comment_meta_whitelist' => true,
'max_enqueue_full_sync' => true,
'max_queue_size_full_sync'=> true,
'sync_via_cron' => true,
'cron_sync_time_limit' => true,
);
static $is_importing;
static $is_doing_cron;
static $is_syncing;
static $is_sending;
static $settings_cache = array(); // some settings can be expensive to compute - let's cache them
static function get_settings() {
$settings = array();
foreach ( array_keys( self::$valid_settings ) as $setting ) {
$settings[ $setting ] = self::get_setting( $setting );
}
return $settings;
}
// Fetches the setting. It saves it if the setting doesn't exist, so that it gets
// autoloaded on page load rather than re-queried every time.
static function get_setting( $setting ) {
if ( ! isset( self::$valid_settings[ $setting ] ) ) {
return false;
}
if ( isset( self::$settings_cache[ $setting ] ) ) {
return self::$settings_cache[ $setting ];
}
$value = get_option( self::SETTINGS_OPTION_PREFIX . $setting );
if ( false === $value ) {
$default_name = "default_$setting"; // e.g. default_dequeue_max_bytes
$value = Jetpack_Sync_Defaults::$$default_name;
update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true );
}
if ( is_numeric( $value ) ) {
$value = intval( $value );
}
$default_array_value = null;
switch( $setting ) {
case 'post_types_blacklist':
$default_array_value = Jetpack_Sync_Defaults::$blacklisted_post_types;
break;
case 'post_meta_whitelist':
$default_array_value = Jetpack_Sync_Defaults::get_post_meta_whitelist();
break;
case 'comment_meta_whitelist':
$default_array_value = Jetpack_Sync_Defaults::get_comment_meta_whitelist();
break;
}
if ( $default_array_value ) {
if ( is_array( $value ) ) {
$value = array_unique( array_merge( $value, $default_array_value ) );
} else {
$value = $default_array_value;
}
}
self::$settings_cache[ $setting ] = $value;
return $value;
}
static function update_settings( $new_settings ) {
$validated_settings = array_intersect_key( $new_settings, self::$valid_settings );
foreach ( $validated_settings as $setting => $value ) {
update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true );
unset( self::$settings_cache[ $setting ] );
// if we set the disabled option to true, clear the queues
if ( 'disable' === $setting && !! $value ) {
require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php';
$listener = Jetpack_Sync_Listener::get_instance();
$listener->get_sync_queue()->reset();
$listener->get_full_sync_queue()->reset();
}
}
}
// returns escapted SQL that can be injected into a WHERE clause
static function get_blacklisted_post_types_sql() {
return 'post_type NOT IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_types_blacklist' ) ) ) . '\')';
}
static function get_whitelisted_post_meta_sql() {
return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_meta_whitelist' ) ) ) . '\')';
}
static function get_whitelisted_comment_meta_sql() {
return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'comment_meta_whitelist' ) ) ) . '\')';
}
static function get_comments_filter_sql() {
return "comment_approved <> 'spam'";
}
static function reset_data() {
$valid_settings = self::$valid_settings;
self::$settings_cache = array();
foreach ( $valid_settings as $option => $value ) {
delete_option( self::SETTINGS_OPTION_PREFIX . $option );
}
self::set_importing( null );
self::set_doing_cron( null );
self::set_is_syncing( null );
self::set_is_sending( null );
}
static function set_importing( $is_importing ) {
// set to NULL to revert to WP_IMPORTING, the standard behaviour
self::$is_importing = $is_importing;
}
static function is_importing() {
if ( ! is_null( self::$is_importing ) ) {
return self::$is_importing;
}
return defined( 'WP_IMPORTING' ) && WP_IMPORTING;
}
static function set_doing_cron( $is_doing_cron ) {
// set to NULL to revert to WP_IMPORTING, the standard behaviour
self::$is_doing_cron = $is_doing_cron;
}
static function is_doing_cron() {
if ( ! is_null( self::$is_doing_cron ) ) {
return self::$is_doing_cron;
}
return defined( 'DOING_CRON' ) && DOING_CRON;
}
static function is_syncing() {
return (bool) self::$is_syncing || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST );
}
static function set_is_syncing( $is_syncing ) {
self::$is_syncing = $is_syncing;
}
static function is_sending() {
return (bool) self::$is_sending;
}
static function set_is_sending( $is_sending ) {
self::$is_sending = $is_sending;
}
}

View File

@@ -0,0 +1,24 @@
<?php
require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-array-codec.php';
/**
* An implementation of iJetpack_Sync_Codec that uses gzip's DEFLATE
* algorithm to compress objects serialized using json_encode
*/
class Jetpack_Sync_Simple_Codec extends Jetpack_Sync_JSON_Deflate_Array_Codec {
const CODEC_NAME = "simple";
public function name() {
return self::CODEC_NAME;
}
public function encode( $object ) {
return base64_encode( $this->json_serialize( $object ) );
}
public function decode( $input ) {
return $this->json_unserialize( base64_decode( $input ) );
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Class Jetpack_Sync_Users
*
* Responsible for syncing user data changes.
*/
class Jetpack_Sync_Users {
static $user_roles = array();
static function init() {
if ( Jetpack::is_active() ) {
// Kick off synchronization of user role when it changes
add_action( 'set_user_role', array( __CLASS__, 'user_role_change' ) );
}
}
/**
* Synchronize connected user role changes
*/
static function user_role_change( $user_id ) {
if ( Jetpack::is_user_connected( $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 );
}
}
static function get_role( $user_id ) {
if ( isset( self::$user_roles[ $user_id ] ) ) {
return self::$user_roles[ $user_id ];
}
$current_user_id = get_current_user_id();
wp_set_current_user( $user_id );
$role = Jetpack::translate_current_user_to_role();
wp_set_current_user( $current_user_id );
$user_roles[ $user_id ] = $role;
return $role;
}
static function get_signed_role( $user_id ) {
return Jetpack::sign_role( self::get_role( $user_id ), $user_id );
}
static function update_role_on_com( $user_id ) {
$signed_role = self::get_signed_role( $user_id );
Jetpack::xmlrpc_async_call( 'jetpack.updateRole', $user_id, $signed_role );
}
static function maybe_demote_master_user( $user_id ) {
$master_user_id = Jetpack_Options::get_option( 'master_user' );
$role = self::get_role( $user_id );
if ( $user_id == $master_user_id && 'administrator' != $role ) {
$query = new WP_User_Query(
array(
'fields' => array( 'id' ),
'role' => 'administrator',
'orderby' => 'id',
'exclude' => array( $master_user_id ),
)
);
$new_master = false;
foreach ( $query->results as $result ) {
$found_user_id = absint( $result->id );
if ( $found_user_id && Jetpack::is_user_connected( $found_user_id ) ) {
$new_master = $found_user_id;
break;
}
}
if ( $new_master ) {
Jetpack_Options::update_option( 'master_user', $new_master );
}
// else disconnect..?
}
}
}
Jetpack_Sync_Users::init();

View File

@@ -0,0 +1,789 @@
<?php
require_once dirname( __FILE__ ) . '/interface.jetpack-sync-replicastore.php';
require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php';
/**
* An implementation of iJetpack_Sync_Replicastore 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
*/
class Jetpack_Sync_WP_Replicastore implements iJetpack_Sync_Replicastore {
public function reset() {
global $wpdb;
$wpdb->query( "DELETE FROM $wpdb->posts" );
$wpdb->query( "DELETE FROM $wpdb->comments" );
// also need to delete terms from cache
$term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" );
foreach ( $term_ids as $term_id ) {
wp_cache_delete( $term_id, 'terms' );
}
$wpdb->query( "DELETE FROM $wpdb->terms" );
$wpdb->query( "DELETE FROM $wpdb->term_taxonomy" );
$wpdb->query( "DELETE FROM $wpdb->term_relationships" );
// callables and constants
$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" );
$wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" );
}
function full_sync_start( $config ) {
$this->reset();
}
function full_sync_end( $checksum ) {
// noop right now
}
public function post_count( $status = null, $min_id = null, $max_id = null ) {
global $wpdb;
$where = '';
if ( $status ) {
$where = "post_status = '" . esc_sql( $status ) . "'";
} else {
$where = '1=1';
}
if ( null != $min_id ) {
$where .= ' AND ID >= ' . intval( $min_id );
}
if ( null != $max_id ) {
$where .= ' AND ID <= ' . intval( $max_id );
}
return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" );
}
// TODO: actually use max_id/min_id
public function get_posts( $status = null, $min_id = null, $max_id = null ) {
$args = array( 'orderby' => 'ID', 'posts_per_page' => -1 );
if ( $status ) {
$args['post_status'] = $status;
} else {
$args['post_status'] = 'any';
}
return get_posts( $args );
}
public function get_post( $id ) {
return get_post( $id );
}
public function upsert_post( $post, $silent = false ) {
global $wpdb;
// reject the post if it's not a WP_Post
if ( ! $post instanceof WP_Post ) {
return;
}
$post = $post->to_array();
// reject posts without an ID
if ( ! isset( $post['ID'] ) ) {
return;
}
$now = current_time( 'mysql' );
$now_gmt = get_gmt_from_date( $now );
$defaults = array(
'ID' => 0,
'post_author' => '0',
'post_content' => '',
'post_content_filtered' => '',
'post_title' => '',
'post_name' => '',
'post_excerpt' => '',
'post_status' => 'draft',
'post_type' => 'post',
'comment_status' => 'closed',
'comment_count' => '0',
'ping_status' => '',
'post_password' => '',
'to_ping' => '',
'pinged' => '',
'post_parent' => 0,
'menu_order' => 0,
'guid' => '',
'post_date' => $now,
'post_date_gmt' => $now_gmt,
'post_modified' => $now,
'post_modified_gmt' => $now_gmt,
);
$post = array_intersect_key( $post, $defaults );
$post = sanitize_post( $post, 'db' );
unset( $post['filter'] );
$exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) );
if ( $exists ) {
$wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) );
} else {
$wpdb->insert( $wpdb->posts, $post );
}
clean_post_cache( $post['ID'] );
}
public function delete_post( $post_id ) {
wp_delete_post( $post_id, true );
}
public function posts_checksum( $min_id = null, $max_id = null ) {
global $wpdb;
return $this->table_checksum( $wpdb->posts, Jetpack_Sync_Defaults::$default_post_checksum_columns , 'ID', Jetpack_Sync_Settings::get_blacklisted_post_types_sql(), $min_id, $max_id );
}
public function post_meta_checksum( $min_id = null, $max_id = null ) {
global $wpdb;
return $this->table_checksum( $wpdb->postmeta, Jetpack_Sync_Defaults::$default_post_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id );
}
public function comment_count( $status = null, $min_id = null, $max_id = null ) {
global $wpdb;
$comment_approved = $this->comment_status_to_approval_value( $status );
if ( $comment_approved !== false ) {
$where = "comment_approved = '" . esc_sql( $comment_approved ) . "'";
} else {
$where = '1=1';
}
if ( $min_id != null ) {
$where .= ' AND comment_ID >= ' . intval( $min_id );
}
if ( $max_id != null ) {
$where .= ' AND comment_ID <= ' . intval( $max_id );
}
return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" );
}
private function comment_status_to_approval_value( $status ) {
switch ( $status ) {
case 'approve':
return '1';
case 'hold':
return '0';
case 'spam':
return 'spam';
case 'trash':
return 'trash';
case 'any':
return false;
case 'all':
return false;
default:
return false;
}
}
// TODO: actually use max_id/min_id
public function get_comments( $status = null, $min_id = null, $max_id = null ) {
$args = array( 'orderby' => 'ID', 'status' => 'all' );
if ( $status ) {
$args['status'] = $status;
}
return get_comments( $args );
}
public function get_comment( $id ) {
return WP_Comment::get_instance( $id );
}
public function upsert_comment( $comment ) {
global $wpdb, $wp_version;
if ( version_compare( $wp_version, '4.4', '<' ) ) {
$comment = (array) $comment;
} else {
// WP 4.4 introduced the WP_Comment Class
$comment = $comment->to_array();
}
// filter by fields on comment table
$comment_fields_whitelist = array(
'comment_ID',
'comment_post_ID',
'comment_author',
'comment_author_email',
'comment_author_url',
'comment_author_IP',
'comment_date',
'comment_date_gmt',
'comment_content',
'comment_karma',
'comment_approved',
'comment_agent',
'comment_type',
'comment_parent',
'user_id',
);
foreach ( $comment as $key => $value ) {
if ( ! in_array( $key, $comment_fields_whitelist ) ) {
unset( $comment[ $key ] );
}
}
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )",
$comment['comment_ID']
)
);
if ( $exists ) {
$wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) );
} else {
$wpdb->insert( $wpdb->comments, $comment );
}
wp_update_comment_count( $comment['comment_post_ID'] );
}
public function trash_comment( $comment_id ) {
wp_delete_comment( $comment_id );
}
public function delete_comment( $comment_id ) {
wp_delete_comment( $comment_id, true );
}
public function spam_comment( $comment_id ) {
wp_spam_comment( $comment_id );
}
public function trashed_post_comments( $post_id, $statuses ) {
wp_trash_post_comments( $post_id );
}
public function untrashed_post_comments( $post_id ) {
wp_untrash_post_comments( $post_id );
}
public function comments_checksum( $min_id = null, $max_id = null ) {
global $wpdb;
return $this->table_checksum( $wpdb->comments, Jetpack_Sync_Defaults::$default_comment_checksum_columns, 'comment_ID', Jetpack_Sync_Settings::get_comments_filter_sql(), $min_id, $max_id );
}
public function comment_meta_checksum( $min_id = null, $max_id = null ) {
global $wpdb;
return $this->table_checksum( $wpdb->commentmeta, Jetpack_Sync_Defaults::$default_comment_meta_checksum_columns , 'meta_id', Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id );
}
public function options_checksum() {
global $wpdb;
$options_whitelist = "'" . implode( "', '", Jetpack_Sync_Defaults::$default_options_whitelist ) . "'";
$where_sql = "option_name IN ( $options_whitelist )";
return $this->table_checksum( $wpdb->options, Jetpack_Sync_Defaults::$default_option_checksum_columns, null, $where_sql, null, null );
}
public function update_option( $option, $value ) {
return update_option( $option, $value );
}
public function get_option( $option, $default = false ) {
return get_option( $option, $default );
}
public function delete_option( $option ) {
return delete_option( $option );
}
public function set_theme_support( $theme_support ) {
// noop
}
public function current_theme_supports( $feature ) {
return current_theme_supports( $feature );
}
public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) {
return get_metadata( $type, $object_id, $meta_key, $single );
}
/**
*
* Stores remote meta key/values alongside an ID mapping key
*
* @param $type
* @param $object_id
* @param $meta_key
* @param $meta_value
* @param $meta_id
*
* @return bool
*/
public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) {
$table = _get_meta_table( $type );
if ( ! $table ) {
return false;
}
global $wpdb;
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )",
$meta_id
) );
if ( $exists ) {
$wpdb->update( $table, array(
'meta_key' => $meta_key,
'meta_value' => maybe_serialize( $meta_value ),
), array( 'meta_id' => $meta_id ) );
} else {
$object_id_field = $type . '_id';
$wpdb->insert( $table, array(
'meta_id' => $meta_id,
$object_id_field => $object_id,
'meta_key' => $meta_key,
'meta_value' => maybe_serialize( $meta_value ),
) );
}
wp_cache_delete( $object_id, $type . '_meta' );
return true;
}
public function delete_metadata( $type, $object_id, $meta_ids ) {
global $wpdb;
$table = _get_meta_table( $type );
if ( ! $table ) {
return false;
}
foreach ( $meta_ids as $meta_id ) {
$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) );
}
// if we don't have an object ID what do we do - invalidate ALL meta?
if ( $object_id ) {
wp_cache_delete( $object_id, $type . '_meta' );
}
}
// todo: test this out to make sure it works as expected.
public function delete_batch_metadata( $type, $object_ids, $meta_key ) {
global $wpdb;
$table = _get_meta_table( $type );
if ( ! $table ) {
return false;
}
$column = sanitize_key($type . '_id' );
$wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) );
// if we don't have an object ID what do we do - invalidate ALL meta?
foreach ( $object_ids as $object_id ) {
wp_cache_delete( $object_id, $type . '_meta' );
}
}
// constants
public function get_constant( $constant ) {
$value = get_option( 'jetpack_constant_' . $constant );
if ( $value ) {
return $value;
}
return null;
}
public function set_constant( $constant, $value ) {
update_option( 'jetpack_constant_' . $constant, $value );
}
public function get_updates( $type ) {
$all_updates = get_option( 'jetpack_updates', array() );
if ( isset( $all_updates[ $type ] ) ) {
return $all_updates[ $type ];
} else {
return null;
}
}
public function set_updates( $type, $updates ) {
$all_updates = get_option( 'jetpack_updates', array() );
$all_updates[ $type ] = $updates;
update_option( 'jetpack_updates', $all_updates );
}
// functions
public function get_callable( $name ) {
$value = get_option( 'jetpack_' . $name );
if ( $value ) {
return $value;
}
return null;
}
public function set_callable( $name, $value ) {
update_option( 'jetpack_' . $name, $value );
}
// network options
public function get_site_option( $option ) {
return get_option( 'jetpack_network_' . $option );
}
public function update_site_option( $option, $value ) {
return update_option( 'jetpack_network_' . $option, $value );
}
public function delete_site_option( $option ) {
return delete_option( 'jetpack_network_' . $option );
}
// terms
// terms
public function get_terms( $taxonomy ) {
return get_terms( $taxonomy );
}
public function get_term( $taxonomy, $term_id, $is_term_id = true ) {
$t = $this->ensure_taxonomy( $taxonomy );
if ( ! $t || is_wp_error( $t ) ) {
return $t;
}
return get_term( $term_id, $taxonomy );
}
private function ensure_taxonomy( $taxonomy ) {
if ( ! taxonomy_exists( $taxonomy ) ) {
// try re-registering synced taxonomies
$taxonomies = $this->get_callable( 'taxonomies' );
if ( ! isset( $taxonomies[ $taxonomy ] ) ) {
// doesn't exist, or somehow hasn't been synced
return new WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" );
}
$t = $taxonomies[ $taxonomy ];
return register_taxonomy(
$taxonomy,
$t->object_type,
(array) $t
);
}
return true;
}
public function get_the_terms( $object_id, $taxonomy ) {
return get_the_terms( $object_id, $taxonomy );
}
public function update_term( $term_object ) {
$taxonomy = $term_object->taxonomy;
global $wpdb;
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )",
$term_object->term_id
) );
if ( ! $exists ) {
$term_object = sanitize_term( clone( $term_object ), $taxonomy, 'db' );
$term = array(
'term_id' => $term_object->term_id,
'name' => $term_object->name,
'slug' => $term_object->slug,
'term_group' => $term_object->term_group,
);
$term_taxonomy = array(
'term_taxonomy_id' => $term_object->term_taxonomy_id,
'term_id' => $term_object->term_id,
'taxonomy' => $term_object->taxonomy,
'description' => $term_object->description,
'parent' => (int) $term_object->parent,
'count' => (int) $term_object->count,
);
$wpdb->insert( $wpdb->terms, $term );
$wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy );
return true;
}
return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object );
}
public function delete_term( $term_id, $taxonomy ) {
return wp_delete_term( $term_id, $taxonomy );
}
public function update_object_terms( $object_id, $taxonomy, $terms, $append ) {
wp_set_object_terms( $object_id, $terms, $taxonomy, $append );
}
public function delete_object_terms( $object_id, $tt_ids ) {
global $wpdb;
if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) {
// escape
$tt_ids_sanitized = array_map( 'intval', $tt_ids );
$taxonomies = array();
foreach ( $tt_ids_sanitized as $tt_id ) {
$term = get_term_by( 'term_taxonomy_id', $tt_id );
$taxonomies[ $term->taxonomy ][] = $tt_id;
}
$in_tt_ids = implode( ", ", $tt_ids_sanitized );
/**
* Fires immediately before an object-term relationship is deleted.
*
* @since 2.9.0
*
* @param int $object_id Object ID.
* @param array $tt_ids An array of term taxonomy IDs.
*/
do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized );
$deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) );
foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) {
$this->ensure_taxonomy( $taxonomy );
wp_cache_delete( $object_id, $taxonomy . '_relationships' );
/**
* Fires immediately after an object-term relationship is deleted.
*
* @since 2.9.0
*
* @param int $object_id Object ID.
* @param array $tt_ids An array of term taxonomy IDs.
*/
do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids );
wp_update_term_count( $taxonomy_tt_ids, $taxonomy );
}
return (bool) $deleted;
}
return false;
}
// users
public function user_count() {
}
public function get_user( $user_id ) {
return WP_User::get_instance( $user_id );
}
public function upsert_user( $user ) {
$this->invalid_call();
}
public function delete_user( $user_id ) {
$this->invalid_call();
}
public function upsert_user_locale( $user_id, $local ) {
$this->invalid_call();
}
public function delete_user_locale( $user_id ) {
$this->invalid_call();
}
public function get_user_locale( $user_id ) {
return jetpack_get_user_locale( $user_id );
}
public function get_allowed_mime_types( $user_id ) {
}
public function checksum_all() {
$post_meta_checksum = $this->checksum_histogram( 'post_meta', 1 );
$comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 );
return array(
'posts' => $this->posts_checksum(),
'comments' => $this->comments_checksum(),
'post_meta'=> reset( $post_meta_checksum ),
'comment_meta'=> reset( $comment_meta_checksum ),
);
}
function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true ) {
global $wpdb;
$wpdb->queries = array();
switch( $object_type ) {
case "posts":
$object_count = $this->post_count( null, $start_id, $end_id );
$object_table = $wpdb->posts;
$id_field = 'ID';
$where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
if ( empty( $columns ) ) {
$columns = Jetpack_Sync_Defaults::$default_post_checksum_columns;
}
break;
case "post_meta":
$object_table = $wpdb->postmeta;
$where_sql = Jetpack_Sync_Settings::get_whitelisted_post_meta_sql();
$object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
$id_field = 'meta_id';
if ( empty( $columns ) ) {
$columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
}
break;
case "comments":
$object_count = $this->comment_count( null, $start_id, $end_id );
$object_table = $wpdb->comments;
$id_field = 'comment_ID';
$where_sql = Jetpack_Sync_Settings::get_comments_filter_sql();
if ( empty( $columns ) ) {
$columns = Jetpack_Sync_Defaults::$default_comment_checksum_columns;
}
break;
case "comment_meta":
$object_table = $wpdb->commentmeta;
$where_sql = Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql();
$object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id );
$id_field = 'meta_id';
if ( empty( $columns ) ) {
$columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns;
}
break;
default:
return false;
}
$bucket_size = intval( ceil( $object_count / $buckets ) );
$previous_max_id = 0;
$histogram = array();
$where = '1=1';
if ( $start_id ) {
$where .= " AND $id_field >= " . intval( $start_id );
}
if ( $end_id ) {
$where .= " AND $id_field <= " . intval( $end_id );
}
do {
list( $first_id, $last_id ) = $wpdb->get_row(
"SELECT MIN($id_field) as min_id, MAX($id_field) as max_id FROM ( SELECT $id_field FROM $object_table WHERE $where AND $id_field > $previous_max_id ORDER BY $id_field ASC LIMIT $bucket_size ) as ids",
ARRAY_N
);
// get the checksum value
$value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $first_id, $last_id, $strip_non_ascii );
if ( is_wp_error( $value ) ) {
return $value;
}
if ( $first_id === null || $last_id === null ) {
break;
} elseif ( $first_id === $last_id ) {
$histogram[ $first_id ] = $value;
} else {
$histogram[ "{$first_id}-{$last_id}" ] = $value;
}
$previous_max_id = $last_id;
} while ( true );
return $histogram;
}
private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true ) {
global $wpdb;
// sanitize to just valid MySQL column names
$sanitized_columns = preg_grep ( '/^[0-9,a-z,A-Z$_]+$/i', $columns );
if ( $strip_non_ascii ) {
$columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns ) );
} else {
$columns_sql = implode( ',', $sanitized_columns );
}
if ( $min_id !== null ) {
$min_id = intval( $min_id );
$where_sql .= " AND $id_column >= $min_id";
}
if ( $max_id !== null ) {
$max_id = intval( $max_id );
$where_sql .= " AND $id_column <= $max_id";
}
$query = <<<ENDSQL
SELECT CONV(BIT_XOR(CRC32(CONCAT({$columns_sql}))), 10, 16)
FROM $table
WHERE $where_sql
ENDSQL;
$result = $wpdb->get_var( $query );
if ( $wpdb->last_error ) {
return new WP_Error( 'database_error', $wpdb->last_error );
}
return $result;
}
private function meta_count( $table, $where_sql, $min_id, $max_id ) {
global $wpdb;
if ( $min_id != null ) {
$where_sql .= ' AND meta_id >= ' . intval( $min_id );
}
if ( $max_id != null ) {
$where_sql .= ' AND meta_id <= ' . intval( $max_id );
}
return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where_sql" );
}
/**
* Wraps a column name in SQL which strips non-ASCII chars.
* This helps normalize data to avoid checksum differences caused by
* badly encoded data in the DB
*/
function strip_non_ascii_sql( $column_name ) {
return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )";
}
private function invalid_call() {
$backtrace = debug_backtrace();
$caller = $backtrace[1]['function'];
throw new Exception( "This function $caller is not supported on the WP Replicastore" );
}
}

View File

@@ -0,0 +1,14 @@
<?php
/**
* Very simple interface for encoding and decoding input
* This is used to provide compression and serialization to messages
**/
interface iJetpack_Sync_Codec {
// we send this with the payload so we can select the appropriate decoder at the other end
public function name();
public function encode( $object );
public function decode( $input );
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* Sync architecture prototype
* @author Dan Walmsley
* To run tests: phpunit --testsuite sync --filter New_Sync
*/
/**
* A high-level interface for objects that store synced WordPress data
* Useful for ensuring that different storage mechanisms implement the
* required semantics for storing all the data that we sync
*/
interface iJetpack_Sync_Replicastore {
// remove all data
public function reset();
// trigger setup for sync start/end
public function full_sync_start( $config );
public function full_sync_end( $checksum );
// posts
public function post_count( $status = null, $min_id = null, $max_id = null );
public function get_posts( $status = null, $min_id = null, $max_id = null );
public function get_post( $id );
public function upsert_post( $post, $silent = false );
public function delete_post( $post_id );
public function posts_checksum( $min_id = null, $max_id = null );
public function post_meta_checksum( $min_id = null, $max_id = null );
// comments
public function comment_count( $status = null, $min_id = null, $max_id = null );
public function get_comments( $status = null, $min_id = null, $max_id = null );
public function get_comment( $id );
public function upsert_comment( $comment );
public function trash_comment( $comment_id );
public function spam_comment( $comment_id );
public function delete_comment( $comment_id );
public function trashed_post_comments( $post_id, $statuses );
public function untrashed_post_comments( $post_id );
public function comments_checksum( $min_id = null, $max_id = null );
public function comment_meta_checksum( $min_id = null, $max_id = null );
// options
public function update_option( $option, $value );
public function get_option( $option, $default = false );
public function delete_option( $option );
// themes
public function set_theme_support( $theme_support );
public function current_theme_supports( $feature );
// meta
public function get_metadata( $type, $object_id, $meta_key = '', $single = false );
public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id );
public function delete_metadata( $type, $object_id, $meta_ids );
public function delete_batch_metadata( $type, $object_ids, $meta_key );
// constants
public function get_constant( $constant );
public function set_constant( $constant, $value );
// updates
public function get_updates( $type );
public function set_updates( $type, $updates );
// functions
public function get_callable( $callable );
public function set_callable( $callable, $value );
// network options
public function get_site_option( $option );
public function update_site_option( $option, $value );
public function delete_site_option( $option );
// terms
public function get_terms( $taxonomy );
public function get_term( $taxonomy, $term_id, $is_term_id = true );
public function update_term( $term_object );
public function delete_term( $term_id, $taxonomy );
public function get_the_terms( $object_id, $taxonomy );
public function update_object_terms( $object_id, $taxonomy, $terms, $append );
public function delete_object_terms( $object_id, $tt_ids );
// users
public function user_count();
public function get_user( $user_id );
public function upsert_user( $user );
public function delete_user( $user_id );
public function upsert_user_locale( $user_id, $locale );
public function delete_user_locale( $user_id );
public function get_user_locale( $user_id );
public function get_allowed_mime_types( $user_id );
// full checksum
public function checksum_all();
// histogram
public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null );
}