summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/sync')
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-actions.php285
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-defaults.php276
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-functions.php157
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-json-deflate-codec.php58
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-listener.php207
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-attachments.php28
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-callables.php148
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-comments.php131
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-constants.php124
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-full-sync.php289
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-meta.php38
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-network-options.php112
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-options.php141
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-plugins.php14
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-posts.php135
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-protect.php16
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-stats.php28
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-terms.php112
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-themes.php71
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-updates.php85
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module-users.php217
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-module.php107
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-modules.php90
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-queue.php419
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-sender.php321
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-server.php106
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-settings.php130
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-users.php81
-rw-r--r--plugins/jetpack/sync/class.jetpack-sync-wp-replicastore.php702
-rw-r--r--plugins/jetpack/sync/interface.jetpack-sync-codec.php14
-rw-r--r--plugins/jetpack/sync/interface.jetpack-sync-replicastore.php129
31 files changed, 4771 insertions, 0 deletions
diff --git a/plugins/jetpack/sync/class.jetpack-sync-actions.php b/plugins/jetpack/sync/class.jetpack-sync-actions.php
new file mode 100644
index 00000000..65eff323
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-actions.php
@@ -0,0 +1,285 @@
+<?php
+require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.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 INITIAL_SYNC_MULTISITE_INTERVAL = 10;
+
+ static function init() {
+
+ // Add a custom "every minute" cron schedule
+ add_filter( 'cron_schedules', array( __CLASS__, 'minute_cron_schedule' ) );
+
+ // On jetpack authorization, schedule a full sync
+ add_action( 'jetpack_client_authorized', array( __CLASS__, 'schedule_full_sync' ) );
+
+ // When imports are finished, schedule a full sync
+ add_action( 'import_end', array( __CLASS__, 'schedule_full_sync' ) );
+
+ // 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';
+
+ // everything below this point should only happen if we're a valid sync site
+ if ( ! self::sync_allowed() ) {
+ return;
+ }
+
+ // publicize filter to prevent publicizing blacklisted post types
+ add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 );
+
+ // cron hooks
+ add_action( 'jetpack_sync_full', array( __CLASS__, 'do_full_sync' ), 10, 1 );
+ add_action( 'jetpack_sync_cron', array( __CLASS__, 'do_cron_sync' ) );
+
+ if ( ! wp_next_scheduled( 'jetpack_sync_cron' ) ) {
+ // Schedule a job to send pending queue items once a minute
+ wp_schedule_event( time(), '1min', 'jetpack_sync_cron' );
+ }
+
+ /**
+ * 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 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',
+ (
+ 'GET' !== $_SERVER['REQUEST_METHOD']
+ ||
+ is_user_logged_in()
+ ||
+ defined( 'PHPUNIT_JETPACK_TESTSUITE' )
+ )
+ ) ) {
+ self::initialize_listener();
+ }
+
+ /**
+ * 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 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',
+ (
+ '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' ) );
+ }
+
+ }
+
+ static function sync_allowed() {
+ return ( ! Jetpack_Sync_Settings::get_setting( 'disable' ) && Jetpack::is_active() && ! ( Jetpack::is_development_mode() || Jetpack::is_staging_site() ) )
+ || defined( 'PHPUNIT_JETPACK_TESTSUITE' );
+ }
+
+ static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) {
+ 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() {
+ Jetpack_Sync_Settings::set_importing( true );
+ }
+
+ static function send_data( $data, $codec_name, $sent_timestamp, $queue_id ) {
+ Jetpack::load_xml_rpc_client();
+
+ $url = add_query_arg( 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
+ ), Jetpack::xmlrpc_api_url() );
+
+ $rpc = new Jetpack_IXR_Client( array(
+ 'url' => $url,
+ 'user_id' => JETPACK_MASTER_USER,
+ 'timeout' => 30,
+ ) );
+
+ $result = $rpc->query( 'jetpack.syncActions', $data );
+
+ if ( ! $result ) {
+ return $rpc->get_jetpack_error();
+ }
+
+ return $rpc->getResponse();
+ }
+
+ static function schedule_initial_sync() {
+ // we need this function call here because we have to run this function
+ // reeeeally early in init, before WP_CRON_LOCK_TIMEOUT is defined.
+ wp_functionality_constants();
+
+ if ( is_multisite() ) {
+ // stagger initial syncs for multisite blogs so they don't all pile on top of each other
+ $time_offset = ( rand() / getrandmax() ) * self::INITIAL_SYNC_MULTISITE_INTERVAL * get_blog_count();
+ } else {
+ $time_offset = 1;
+ }
+
+ self::schedule_full_sync(
+ array(
+ 'options' => true,
+ 'network_options' => true,
+ 'functions' => true,
+ 'constants' => true,
+ 'users' => 'initial'
+ ),
+ $time_offset
+ );
+ }
+
+ static function schedule_full_sync( $modules = null, $time_offset = 1 ) {
+ if ( ! self::sync_allowed() ) {
+ return false;
+ }
+
+ if ( self::is_scheduled_full_sync() ) {
+ self::unschedule_all_full_syncs();
+ }
+
+ if ( $modules ) {
+ wp_schedule_single_event( time() + $time_offset, 'jetpack_sync_full', array( $modules ) );
+ } else {
+ wp_schedule_single_event( time() + $time_offset, 'jetpack_sync_full' );
+ }
+
+ if ( $time_offset === 1 ) {
+ spawn_cron();
+ }
+
+ return true;
+ }
+
+ static function unschedule_all_full_syncs() {
+ foreach ( _get_cron_array() as $timestamp => $cron ) {
+ if ( ! empty( $cron['jetpack_sync_full'] ) ) {
+ foreach( $cron['jetpack_sync_full'] as $key => $config ) {
+ wp_unschedule_event( $timestamp, 'jetpack_sync_full', $config['args'] );
+ }
+ }
+ }
+ }
+
+ static function is_scheduled_full_sync( $modules = null ) {
+ if ( is_null( $modules ) ) {
+ $crons = _get_cron_array();
+
+ foreach ( $crons as $timestamp => $cron ) {
+ if ( ! empty( $cron['jetpack_sync_full'] ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return wp_next_scheduled( 'jetpack_sync_full', array( $modules ) );
+ }
+
+ static function do_full_sync( $modules = null ) {
+ if ( ! self::sync_allowed() ) {
+ return;
+ }
+
+ self::initialize_listener();
+ Jetpack_Sync_Modules::get_module( 'full-sync' )->start( $modules );
+ self::do_cron_sync(); // immediately run a cron sync, which sends pending data
+ }
+
+ static function minute_cron_schedule( $schedules ) {
+ if( ! isset( $schedules["1min"] ) ) {
+ $schedules["1min"] = array(
+ 'interval' => 60,
+ 'display' => __( 'Every minute' )
+ );
+ }
+ return $schedules;
+ }
+
+ // 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
+ static function do_cron_sync() {
+ if ( ! self::sync_allowed() ) {
+ return;
+ }
+
+ self::initialize_sender();
+
+ // remove shutdown hook - no need to sync twice
+ if ( has_action( 'shutdown', array( self::$sender, 'do_sync' ) ) ) {
+ remove_action( 'shutdown', array( self::$sender, 'do_sync' ) );
+ }
+
+ do {
+ $next_sync_time = self::$sender->get_next_sync_time();
+
+ if ( $next_sync_time ) {
+ $delay = $next_sync_time - time() + 1;
+ if ( $delay > 15 ) {
+ break;
+ } elseif ( $delay > 0 ) {
+ sleep( $delay );
+ }
+ }
+
+ $result = self::$sender->do_sync();
+ } while ( $result );
+ }
+
+ 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, 4 );
+ }
+}
+
+// Allow other plugins to add filters before we initialize the actions.
+// Load the listeners if before modules get loaded so that we can capture version changes etc.
+add_action( 'init', 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', 'schedule_initial_sync' ), 10 );
diff --git a/plugins/jetpack/sync/class.jetpack-sync-defaults.php b/plugins/jetpack/sync/class.jetpack-sync-defaults.php
new file mode 100644
index 00000000..f08a8268
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-defaults.php
@@ -0,0 +1,276 @@
+<?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',
+ 'home',
+ 'siteurl',
+ 'blogdescription',
+ 'blog_charset',
+ 'permalink_structure',
+ 'category_base',
+ 'tag_base',
+ 'comment_moderation',
+ 'default_comment_status',
+ 'jetpack_site_icon_url',
+ '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',
+ 'lang_id',
+ '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_themes',
+ 'jetpack_autoupdate_core',
+ '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',
+ 'sharing_services',
+ 'jetpack_sso_require_two_step',
+ 'jetpack_relatedposts',
+ 'verification_services_codes',
+ 'users_can_register',
+ 'active_plugins',
+ 'uninstall_plugins',
+ );
+
+ 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',
+ );
+
+ 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' ),
+ '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' ),
+ 'active_modules' => array( 'Jetpack', 'get_active_modules' ),
+ );
+
+ static $blacklisted_post_types = array(
+ 'ai1ec_event',
+ 'snitch',
+ );
+
+ static $default_post_checksum_columns = array(
+ 'ID',
+ 'post_modified',
+ );
+
+ static $default_comment_checksum_columns = array(
+ 'comment_ID',
+ 'comment_content',
+ );
+
+ 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' ),
+ );
+
+
+ static $default_whitelist_meta_keys = array(
+ '_wp_attachment_metadata',
+ '_thumbnail_id',
+ '_wpas_mess',
+ '_wpas_skip_',
+ '_g_feedback_shortcode',
+ '_feedback_extra_fields',
+ '_feedback_akismet_values',
+ '_publicize_facebook_user',
+ '_wp_attachment_image_alt',
+ '_jetpack_post_thumbnail',
+ '_thumbnail_id',
+ '_wp_attachment_metadata',
+ '_wp_page_template',
+ '_publicize_twitter_user',
+ '_wp_trash_meta_comments_status',
+ );
+
+ static $default_blacklist_meta_keys = array(
+ 'post_views_count',
+ 'Views',
+ 'tve_leads_impressions',
+ 'views',
+ 'scc_share_count_crawldate',
+ 'wprss_last_update',
+ 'wprss_feed_is_updating',
+ 'snapFB',
+ 'syndication_item_hash',
+ 'phonenumber_spellings',
+ 'tmac_last_id',
+ 'opanda_imperessions',
+ 'administer_stats',
+ 'spec_ads_views',
+ 'snp_views',
+ 'mip_post_views_count',
+ 'esml_socialcount_LAST_UPDATED',
+ 'wprss_last_update_items',
+ );
+
+ // 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_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_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_meta_blacklist = array();
+ static $default_disable = 0; // completely disable sending data to wpcom
+ 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
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-functions.php b/plugins/jetpack/sync/class.jetpack-sync-functions.php
new file mode 100644
index 00000000..9ce70f70
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-functions.php
@@ -0,0 +1,157 @@
+<?php
+
+/*
+ * Utility functions to generate data synced to wpcom
+ */
+
+class Jetpack_Sync_Functions {
+
+ 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;
+
+ return $wp_taxonomies;
+ }
+
+ public static function get_post_types() {
+ global $wp_post_types;
+
+ return $wp_post_types;
+ }
+
+ public static function get_post_type_features() {
+ global $_wp_post_type_features;
+
+ return $_wp_post_type_features;
+ }
+
+ 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();
+ $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
+ ob_end_clean();
+ if ( $filesystem_credentials_are_stored ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function home_url() {
+ return self::preserve_scheme( 'home', 'home_url', true );
+ }
+
+ public static function site_url() {
+ return self::preserve_scheme( 'siteurl', 'site_url', true );
+ }
+
+ public static function main_network_site_url() {
+ return self::preserve_scheme( 'siteurl', 'network_site_url', false );
+ }
+
+ public static function preserve_scheme( $option, $url_function, $normalize_www = false ) {
+ $previous_https_value = isset( $_SERVER['HTTPS'] ) ? $_SERVER['HTTPS'] : null;
+ $_SERVER['HTTPS'] = 'off';
+ $url = call_user_func( $url_function );
+ $option_url = get_option( $option );
+ if ( $previous_https_value ) {
+ $_SERVER['HTTPS'] = $previous_https_value;
+ } else {
+ unset( $_SERVER['HTTPS'] );
+ }
+
+ if ( $option_url === $url ) {
+ return $url;
+ }
+
+ // turn them both into parsed format
+ $option_url = parse_url( $option_url );
+ $url = parse_url( $url );
+
+ if ( $normalize_www ) {
+ 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'];
+ }
+ }
+
+ if ( $url['host'] === $option_url['host'] ) {
+ $url['scheme'] = $option_url['scheme'];
+ // return set_url_scheme( $current_url, $option_url['scheme'] );
+ }
+
+ $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() );
+ }
+
+ public static function wp_version() {
+ global $wp_version;
+
+ return $wp_version;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-json-deflate-codec.php b/plugins/jetpack/sync/class.jetpack-sync-json-deflate-codec.php
new file mode 100644
index 00000000..6ec966e9
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-json-deflate-codec.php
@@ -0,0 +1,58 @@
+<?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_Codec implements iJetpack_Sync_Codec {
+ const CODEC_NAME = 'deflate-json';
+
+ public function name() {
+ return self::CODEC_NAME;
+ }
+
+ public function encode( $object ) {
+ return base64_encode( gzdeflate( $this->json_serialize( unserialize( serialize( $object ) ) ) ) );
+ }
+
+ public function decode( $input ) {
+ return $this->json_unserialize( gzinflate( base64_decode( $input ) ) );
+ }
+
+ // @see https://gist.github.com/muhqu/820694
+ private function json_serialize( $any ) {
+ return json_encode( $this->json_wrap( $any ) );
+ }
+
+ private function json_unserialize( $str ) {
+ return $this->json_unwrap( json_decode( $str ) );
+ }
+
+ private function json_wrap( $any, $skip_assoc = false ) {
+ if ( ! $skip_assoc && is_array( $any ) && is_string( key( $any ) ) ) {
+ return (object) array( '_PHP_ASSOC' => $this->json_wrap( $any, true ) );
+ }
+ if ( is_array( $any ) || is_object( $any ) ) {
+ foreach ( $any as &$v ) {
+ $v = $this->json_wrap( $v );
+ }
+ }
+
+ return $any;
+ }
+
+ private function json_unwrap( $any, $skip_assoc = false ) {
+ if ( ! $skip_assoc && is_object( $any ) && isset( $any->_PHP_ASSOC ) && count( (array) $any ) == 1 ) {
+ return (array) $this->json_unwrap( $any->_PHP_ASSOC );
+ }
+ if ( is_array( $any ) || is_object( $any ) ) {
+ foreach ( $any as &$v ) {
+ $v = $this->json_unwrap( $v );
+ }
+ }
+
+ return $any;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-listener.php b/plugins/jetpack/sync/class.jetpack-sync-listener.php
new file mode 100644
index 00000000..1362084e
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-listener.php
@@ -0,0 +1,207 @@
+<?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';
+
+/**
+ * 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 );
+
+ // 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
+ *
+ * @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 ) {
+ /**
+ * 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 );
+ }
+
+ $queue->add( array(
+ $current_filter,
+ $args,
+ get_current_user_id(),
+ microtime( true ),
+ Jetpack_Sync_Settings::is_importing()
+ ) );
+ }
+
+ 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' ) );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-attachments.php b/plugins/jetpack/sync/class.jetpack-sync-module-attachments.php
new file mode 100644
index 00000000..4cee033e
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-attachments.php
@@ -0,0 +1,28 @@
+<?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_add_attachment', $callable, 10, 2 );
+ }
+
+ function send_attachment_info( $attachment_id ) {
+ $attachment = get_post( $attachment_id );
+
+ /**
+ * Fires when the client needs to sync an attachment for a post
+ *
+ * @since 4.2.0
+ *
+ * @param int The attachment ID
+ * @param object The attachment
+ */
+ do_action( 'jetpack_sync_save_add_attachment', $attachment_id, $attachment );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-callables.php b/plugins/jetpack/sync/class.jetpack-sync-module-callables.php
new file mode 100644
index 00000000..d2d8bc5a
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-callables.php
@@ -0,0 +1,148 @@
+<?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::$default_callable_whitelist, Jetpack_Sync_Defaults::$default_multisite_callable_whitelist );
+ } else {
+ $this->callable_whitelist = Jetpack_Sync_Defaults::$default_callable_whitelist;
+ }
+ }
+
+ public function init_listeners( $callable ) {
+ add_action( 'jetpack_sync_callable', $callable, 10, 2 );
+
+ // always send change to active modules right away
+ add_action( 'update_option_jetpack_active_modules', 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_sync_callable' ) );
+ }
+
+ 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 );
+ }
+
+ 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->callable_whitelist ),
+ array_map( array( $this, 'get_callable' ), array_values( $this->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 ) {
+ /**
+ * 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 );
+
+ return 1; // The number of actions enqueued
+ }
+
+ 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 maybe_sync_callables() {
+ 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) get_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 ( ! $this->still_valid_checksum( $callable_checksums, $name, $checksum ) && ! is_null( $value ) ) {
+ /**
+ * 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;
+ }
+ }
+ update_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
+ }
+
+ public function expand_callables( $args ) {
+ if ( $args[0] ) {
+ return $this->get_all_callables();
+ }
+
+ return $args;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-comments.php b/plugins/jetpack/sync/class.jetpack-sync-module-comments.php
new file mode 100644
index 00000000..3c93a8d6
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-comments.php
@@ -0,0 +1,131 @@
+<?php
+
+class Jetpack_Sync_Module_Comments extends Jetpack_Sync_Module {
+
+ public function name() {
+ return 'comments';
+ }
+
+ 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 );
+
+ // 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 );
+ }
+ }
+ }
+
+ 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 ) {
+ global $wpdb;
+ return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', $this->get_where_sql( $config ) );
+ }
+
+ 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;
+ }
+
+ 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' ),
+ );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-constants.php b/plugins/jetpack/sync/class.jetpack-sync-module-constants.php
new file mode 100644
index 00000000..4ad9bafa
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-constants.php
@@ -0,0 +1,124 @@
+<?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';
+ }
+
+ private $constants_whitelist;
+
+ public function set_defaults() {
+ $this->constants_whitelist = Jetpack_Sync_Defaults::$default_constants_whitelist;
+ }
+
+ 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 $this->constants_whitelist;
+ }
+
+ function enqueue_full_sync_actions( $config ) {
+ /**
+ * 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 );
+
+ return 1;
+ }
+
+ 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() {
+ return array_combine(
+ $this->constants_whitelist,
+ array_map( array( $this, 'get_constant' ), $this->constants_whitelist )
+ );
+ }
+
+ private function get_constant( $constant ) {
+ return ( defined( $constant ) ) ?
+ constant( $constant )
+ : null;
+ }
+
+ public function expand_constants( $args ) {
+ if ( $args[0] ) {
+ return $this->get_all_constants();
+ }
+
+ return $args;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-full-sync.php b/plugins/jetpack/sync/class.jetpack-sync-module-full-sync.php
new file mode 100644
index 00000000..1f196d1e
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-full-sync.php
@@ -0,0 +1,289 @@
+<?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.
+ */
+
+require_once 'class.jetpack-sync-wp-replicastore.php';
+
+class Jetpack_Sync_Module_Full_Sync extends Jetpack_Sync_Module {
+ const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
+ const FULL_SYNC_TIMEOUT = 3600;
+
+ private $items_added_since_last_pause;
+ private $last_pause_time;
+ private $queue_rate_limit;
+
+ 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( $modules = null ) {
+ $was_already_running = $this->is_started() && ! $this->is_finished();
+
+ // remove all evidence of previous full sync items and status
+ $this->reset_data();
+
+ $this->enable_queue_rate_limit();
+
+ if ( $was_already_running ) {
+ /**
+ * Fires when a full sync is cancelled.
+ *
+ * @since 4.2.0
+ */
+ do_action( 'jetpack_full_sync_cancelled' );
+ }
+
+ /**
+ * 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', $modules );
+ $this->update_status_option( 'started', time() );
+
+ // configure modules
+ if ( ! is_array( $modules ) ) {
+ $modules = array();
+ }
+
+ if ( isset( $modules['users'] ) && 'initial' === $modules['users'] ) {
+ $user_module = Jetpack_Sync_Modules::get_module( 'users' );
+ $modules['users'] = $user_module->get_initial_sync_user_config();
+ }
+
+ // by default, all modules are fully enabled
+ if ( count( $modules ) === 0 ) {
+ $default_module_config = true;
+ } else {
+ $default_module_config = false;
+ }
+
+ // set default configuration, calculate totals, and save configuration if totals > 0
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ $module_name = $module->name();
+ if ( ! isset( $modules[ $module_name ] ) ) {
+ $modules[ $module_name ] = $default_module_config;
+ }
+
+ // check if this module is enabled
+ if ( ! ( $module_config = $modules[ $module_name ] ) ) {
+ continue;
+ }
+
+ $total_items = $module->estimate_full_sync_actions( $module_config );
+
+ if ( ! is_null( $total_items ) && $total_items > 0 ) {
+ $this->update_status_option( "{$module_name}_total", $total_items );
+ $this->update_status_option( "{$module_name}_config", $module_config );
+ }
+ }
+
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ $module_name = $module->name();
+ $module_config = $modules[ $module_name ];
+
+ // check if this module is enabled
+ if ( ! $module_config ) {
+ continue;
+ }
+
+ $items_enqueued = $module->enqueue_full_sync_actions( $module_config );
+
+ if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
+ $this->update_status_option( "{$module_name}_queued", $items_enqueued );
+ }
+ }
+
+ $this->update_status_option( 'queue_finished', time() );
+
+ $store = new Jetpack_Sync_WP_Replicastore();
+
+ /**
+ * Fires when a full sync ends. This action is serialized
+ * and sent to the server with checksums so that we can confirm the
+ * sync was successful.
+ *
+ * @since 4.2.0
+ */
+ do_action( 'jetpack_full_sync_end', $store->checksum_all() );
+
+ $this->disable_queue_rate_limit();
+
+ return true;
+ }
+
+ 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_map( 'reset', $actions ) );
+
+ if ( ! $this->is_started() || $this->is_finished() ) {
+ return;
+ }
+
+ if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
+ $this->update_status_option( 'sent_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 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' ),
+ 'sent_started' => $this->get_status_option( 'sent_started' ),
+ 'finished' => $this->get_status_option( 'finished' ),
+ 'sent' => array(),
+ 'queue' => array(),
+ 'config' => array(),
+ 'total' => array(),
+ );
+
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ $name = $module->name();
+
+ if ( $total = $this->get_status_option( "{$name}_total" ) ) {
+ $status[ 'total' ][ $name ] = $total;
+ }
+
+ if ( $queued = $this->get_status_option( "{$name}_queued" ) ) {
+ $status[ 'queue' ][ $name ] = $queued;
+ }
+
+ if ( $sent = $this->get_status_option( "{$name}_sent" ) ) {
+ $status[ 'sent' ][ $name ] = $sent;
+ }
+
+ if ( $config = $this->get_status_option( "{$name}_config" ) ) {
+ $status[ 'config' ][ $name ] = $config;
+ }
+ }
+
+ return $status;
+ }
+
+ public function clear_status() {
+ $prefix = self::STATUS_OPTION_PREFIX;
+ delete_option( "{$prefix}_started" );
+ delete_option( "{$prefix}_queue_finished" );
+ delete_option( "{$prefix}_sent_started" );
+ delete_option( "{$prefix}_finished" );
+
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ delete_option( "{$prefix}_{$module->name()}_total" );
+ delete_option( "{$prefix}_{$module->name()}_queued" );
+ delete_option( "{$prefix}_{$module->name()}_sent" );
+ delete_option( "{$prefix}_{$module->name()}_config" );
+ }
+ }
+
+ public function reset_data() {
+ $this->clear_status();
+ 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( $option, $default = null ) {
+ $prefix = self::STATUS_OPTION_PREFIX;
+
+ $value = get_option( "{$prefix}_{$option}", $default );
+
+ if ( ! $value ) {
+ // don't cast to int if we didn't find a value - we want to preserve null or false as sentinals
+ return $default;
+ }
+
+ return is_numeric( $value ) ? intval( $value ) : $value;
+ }
+
+ private function update_status_option( $name, $value ) {
+ $prefix = self::STATUS_OPTION_PREFIX;
+ update_option( "{$prefix}_{$name}", $value, false );
+ }
+
+ private function enable_queue_rate_limit() {
+ $this->queue_rate_limit = Jetpack_Sync_Settings::get_setting( 'queue_max_writes_sec' );
+ $this->items_added_since_last_pause = 0;
+ $this->last_pause_time = microtime( true );
+
+ add_action( 'jpsq_item_added', array( $this, 'queue_item_added' ) );
+ add_action( 'jpsq_items_added', array( $this, 'queue_items_added' ) );
+ }
+
+ private function disable_queue_rate_limit() {
+ remove_action( 'jpsq_item_added', array( $this, 'queue_item_added' ) );
+ remove_action( 'jpsq_items_added', array( $this, 'queue_items_added' ) );
+ }
+
+ public function queue_item_added() {
+ $this->queue_items_added( 1 );
+ }
+
+ public function queue_items_added( $item_count ) {
+ // jpsq_item_added and jpsq_items_added both exec 1 db query,
+ // so we ignore $item_count and treat it as always 1
+ $this->items_added_since_last_pause += 1;
+
+ if ( $this->items_added_since_last_pause > $this->queue_rate_limit ) {
+ // sleep for the rest of the second
+ $sleep_til = $this->last_pause_time + 1.0;
+ $sleep_duration = $sleep_til - microtime( true );
+ if ( $sleep_duration > 0.0 ) {
+ usleep( $sleep_duration * 1000000 );
+ $this->last_pause_time = microtime( true );
+ }
+ $this->items_added_since_last_pause = 0;
+ }
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-meta.php b/plugins/jetpack/sync/class.jetpack-sync-module-meta.php
new file mode 100644
index 00000000..5fff16a3
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-meta.php
@@ -0,0 +1,38 @@
+<?php
+
+class Jetpack_Sync_Module_Meta extends Jetpack_Sync_Module {
+ private $meta_types = array( 'post', 'comment' );
+
+ public function name() {
+ return 'meta';
+ }
+
+ public function init_listeners( $callable ) {
+ $whitelist_handler = array( $this, 'filter_meta' );
+
+ foreach ( $this->meta_types as $meta_type ) {
+ 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 );
+
+ 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 );
+ }
+ }
+
+ function filter_meta( $args ) {
+ if ( '_' === $args[2][0] &&
+ ! in_array( $args[2], Jetpack_Sync_Defaults::$default_whitelist_meta_keys ) &&
+ ! wp_startswith( $args[2], '_wpas_skip_' )
+ ) {
+ return false;
+ }
+
+ if ( in_array( $args[2], Jetpack_Sync_Settings::get_setting( 'meta_blacklist' ) ) ) {
+ return false;
+ }
+
+ return $args;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-network-options.php b/plugins/jetpack/sync/class.jetpack-sync-module-network-options.php
new file mode 100644
index 00000000..b677890f
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-network-options.php
@@ -0,0 +1,112 @@
+<?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 ) {
+ if ( ! is_multisite() ) {
+ return 0;
+ }
+
+ /**
+ * 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 );
+
+ return 1; // The number of actions enqueued
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-options.php b/plugins/jetpack/sync/class.jetpack-sync-module-options.php
new file mode 100644
index 00000000..a708d49d
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-options.php
@@ -0,0 +1,141 @@
+<?php
+
+class Jetpack_Sync_Module_Options extends Jetpack_Sync_Module {
+ private $options_whitelist;
+
+ 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();
+ }
+
+ function enqueue_full_sync_actions( $config ) {
+ /**
+ * 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 );
+
+ return 1; // The number of actions enqueued
+ }
+
+ 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();
+ foreach ( $this->options_whitelist as $option ) {
+ $options[ $option ] = get_option( $option );
+ }
+
+ // add theme mods
+ $theme_mods_option = 'theme_mods_'.get_option( 'stylesheet' );
+ $theme_mods_value = get_option( $theme_mods_option );
+ $this->filter_theme_mods( $theme_mods_value );
+ $options[ $theme_mods_option ] = $theme_mods_value;
+
+ return $options;
+ }
+
+ function update_options_whitelist() {
+ /** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */
+ $this->options_whitelist = apply_filters( 'jetpack_options_whitelist', Jetpack_Sync_Defaults::$default_options_whitelist );
+ }
+
+ function set_options_whitelist( $options ) {
+ $this->options_whitelist = $options;
+ }
+
+ function get_options_whitelist() {
+ return $this->options_whitelist;
+ }
+
+ // reject non-whitelisted options
+ function whitelist_options( $args ) {
+ 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] );
+ }
+ }
+
+ return $args;
+ }
+
+ function is_whitelisted_option( $option ) {
+ return in_array( $option, $this->options_whitelist ) || 'theme_mods_' === substr( $option, 0, 11 );
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-plugins.php b/plugins/jetpack/sync/class.jetpack-sync-module-plugins.php
new file mode 100644
index 00000000..e2b3dd8f
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-plugins.php
@@ -0,0 +1,14 @@
+<?php
+
+class Jetpack_Sync_Module_Plugins extends Jetpack_Sync_Module {
+
+ public function name() {
+ return 'plugins';
+ }
+
+ public function init_listeners( $callable ) {
+ add_action( 'deleted_plugin', $callable, 10, 2 );
+ add_action( 'activated_plugin', $callable, 10, 2 );
+ add_action( 'deactivated_plugin', $callable, 10, 2 );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-posts.php b/plugins/jetpack/sync/class.jetpack-sync-module-posts.php
new file mode 100644
index 00000000..8e2bec28
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-posts.php
@@ -0,0 +1,135 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php';
+
+class Jetpack_Sync_Module_Posts extends Jetpack_Sync_Module {
+
+ public function name() {
+ return 'posts';
+ }
+
+ public function set_defaults() {
+ }
+
+ public function init_listeners( $callable ) {
+ add_action( 'wp_insert_post', $callable, 10, 3 );
+ add_action( 'deleted_post', $callable, 10 );
+ add_action( 'jetpack_publicize_post', $callable );
+ add_filter( 'jetpack_sync_before_enqueue_wp_insert_post', array( $this, 'filter_blacklisted_post_types' ) );
+ }
+
+ 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_wp_insert_post', array( $this, 'expand_wp_insert_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 ) {
+ global $wpdb;
+
+ return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ) );
+ }
+
+ 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
+ */
+
+ function expand_wp_insert_post( $args ) {
+ return array( $args[0], $this->filter_post_content_and_add_links( $args[1] ), $args[2] );
+ }
+
+ 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;
+ }
+
+ // Expands wp_insert_post to include filtered content
+ function filter_post_content_and_add_links( $post_object ) {
+ global $post;
+ $post = $post_object;
+ /**
+ * 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;
+ }
+
+ 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 */
+ $post->post_content_filtered = apply_filters( 'the_content', $post->post_content );
+ $post->post_excerpt_filtered = apply_filters( 'the_content', $post->post_excerpt );
+ $post->permalink = get_permalink( $post->ID );
+ $post->shortlink = wp_get_shortlink( $post->ID );
+ $post->dont_email_post_to_subs = Jetpack::is_module_active( 'subscriptions' ) ?
+ get_post_meta( $post->ID, '_jetpack_dont_email_post_to_subs', true ) :
+ true; // Don't email subscription if the subscription module is not active.
+
+ return $post;
+ }
+
+ 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 );
+
+ return array(
+ $posts,
+ $this->get_metadata( $post_ids, 'post' ),
+ $this->get_term_relationships( $post_ids ),
+ );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-protect.php b/plugins/jetpack/sync/class.jetpack-sync-module-protect.php
new file mode 100644
index 00000000..4a07c6b7
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-protect.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * logs bruteprotect failed logins via sync
+ */
+class Jetpack_Sync_Module_Protect extends Jetpack_Sync_Module {
+ private $taxonomy_whitelist;
+
+ function name() {
+ return 'protect';
+ }
+
+ function init_listeners( $callback ) {
+ add_action( 'jpp_log_failed_attempt', $callback );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-stats.php b/plugins/jetpack/sync/class.jetpack-sync-module-stats.php
new file mode 100644
index 00000000..e7a3695a
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-stats.php
@@ -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 ) );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-terms.php b/plugins/jetpack/sync/class.jetpack-sync-module-terms.php
new file mode 100644
index 00000000..427e13c8
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-terms.php
@@ -0,0 +1,112 @@
+<?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, 10, 4 );
+ 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 ) {
+ global $wpdb;
+
+ $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 $total_chunks_counter;
+ }
+
+ 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 );
+ }
+
+ /**
+ * Fires when the client needs to sync a new 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-themes.php b/plugins/jetpack/sync/class.jetpack-sync-module-themes.php
new file mode 100644
index 00000000..16f0451d
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-themes.php
@@ -0,0 +1,71 @@
+<?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 );
+ }
+
+ 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 ) {
+ /**
+ * 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 );
+ return 1; // The number of actions enqueued
+ }
+
+ 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() );
+ }
+
+ 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 ];
+ }
+ }
+
+ return $theme_support;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-updates.php b/plugins/jetpack/sync/class.jetpack-sync-module-updates.php
new file mode 100644
index 00000000..f3f23774
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-updates.php
@@ -0,0 +1,85 @@
+<?php
+
+class Jetpack_Sync_Module_Updates extends Jetpack_Sync_Module {
+ function name() {
+ return 'updates';
+ }
+
+ public function init_listeners( $callable ) {
+ add_action( 'set_site_transient_update_plugins', $callable, 10, 1 );
+ add_action( 'set_site_transient_update_themes', $callable, 10, 1 );
+ add_action( 'set_site_transient_update_core', $callable, 10, 1 );
+
+ add_filter( 'jetpack_sync_before_enqueue_set_site_transient_update_plugins', array(
+ $this,
+ 'filter_update_keys',
+ ), 10, 2 );
+ add_filter( 'jetpack_sync_before_enqueue_upgrader_process_complete', array(
+ $this,
+ 'filter_upgrader_process_complete',
+ ), 10, 2 );
+ }
+
+ public function init_full_sync_listeners( $callable ) {
+ add_action( 'jetpack_full_sync_updates', $callable );
+ }
+
+ public function init_before_send() {
+ // full sync
+ add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) );
+ }
+
+ public function enqueue_full_sync_actions( $config ) {
+ /**
+ * 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 );
+
+ return 1; // The number of actions enqueued
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-users.php b/plugins/jetpack/sync/class.jetpack-sync-module-users.php
new file mode 100644
index 00000000..2b6e1c09
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module-users.php
@@ -0,0 +1,217 @@
+<?php
+
+class Jetpack_Sync_Module_Users extends Jetpack_Sync_Module {
+ const MAX_INITIAL_SYNC_USERS = 100;
+
+ function name() {
+ return 'users';
+ }
+
+ public function init_listeners( $callable ) {
+ // users
+ add_action( 'user_register', array( $this, 'save_user_handler' ) );
+ add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 );
+ add_action( 'add_user_to_blog', array( $this, 'save_user_handler' ) );
+ add_action( 'jetpack_sync_save_user', $callable, 10, 2 );
+
+ add_action( 'deleted_user', $callable, 10, 2 );
+ add_action( 'remove_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, 'save_user_cap_handler' ), 10, 4 );
+ add_action( 'updated_user_meta', array( $this, 'save_user_cap_handler' ), 10, 4 );
+ add_action( 'deleted_user_meta', array( $this, 'save_user_cap_handler' ), 10, 4 );
+
+ // user authentication
+ add_action( 'wp_login', $callable, 10, 2 );
+ add_action( 'wp_login_failed', $callable, 10, 2 );
+ add_action( 'wp_logout', $callable, 10, 0 );
+ }
+
+ 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_jetpack_sync_save_user', array( $this, 'expand_user' ) );
+ 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' ) );
+ }
+
+ public function sanitize_user_and_expand( $user ) {
+ $user = $this->sanitize_user( $user );
+
+ return $this->add_to_user( $user );
+ }
+
+ public function sanitize_user( $user ) {
+ // this create a new user object and stops the passing of the object by reference.
+ $user = unserialize( serialize( $user ) );
+ unset( $user->data->user_pass );
+
+ return $user;
+ }
+
+ public function add_to_user( $user ) {
+ $user->allowed_mime_types = get_allowed_mime_types( $user );
+
+ return $user;
+ }
+
+ public function expand_user( $args ) {
+ list( $user ) = $args;
+
+ if ( $user ) {
+ return array( $this->add_to_user( $user ) );
+ }
+
+ 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 = $user->data->user_login;
+
+ return array( $login, $user );
+ }
+
+ 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 = $this->sanitize_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 ) {
+ unset( $old_user->user_pass );
+ if ( serialize( $old_user ) === serialize( $user->data ) ) {
+ return;
+ }
+ }
+ /**
+ * Fires when the client needs to sync an updated user
+ *
+ * @since 4.2.0
+ *
+ * @param object The WP_User object
+ */
+ do_action( 'jetpack_sync_save_user', $user );
+ }
+
+ function save_user_role_handler( $user_id, $role, $old_roles = null ) {
+ $user = $this->sanitize_user( get_user_by( 'id', $user_id ) );
+
+ /**
+ * Fires when the client needs to sync an updated user
+ *
+ * @since 4.2.0
+ *
+ * @param object The WP_User object
+ */
+ do_action( 'jetpack_sync_save_user', $user );
+ }
+
+ function save_user_cap_handler( $meta_id, $user_id, $meta_key, $capabilities ) {
+
+ // if a user is currently being removed as a member of this blog, we don't fire the event
+ if ( current_filter() === 'deleted_user_meta'
+ &&
+ preg_match( '/capabilities|user_level/', $meta_key )
+ &&
+ ! is_user_member_of_blog( $user_id, get_current_blog_id() )
+ ) {
+ return;
+ }
+
+ $user = get_user_by( 'id', $user_id );
+ if ( $meta_key === $user->cap_key ) {
+ /**
+ * Fires when the client needs to sync an updated user
+ *
+ * @since 4.2.0
+ *
+ * @param object The Sanitized WP_User object
+ */
+ do_action( 'jetpack_sync_save_user', $this->sanitize_user( $user ) );
+ }
+ }
+
+ public function enqueue_full_sync_actions( $config ) {
+ global $wpdb;
+ return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ) );
+ }
+
+ 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 ) ) );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-module.php b/plugins/jetpack/sync/class.jetpack-sync-module.php
new file mode 100644
index 00000000..bfcada54
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-module.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * Basic methods implemented by Jetpack Sync extensions
+ */
+abstract class Jetpack_Sync_Module {
+ const ARRAY_CHUNK_SIZE = 10;
+
+ abstract public function name();
+
+ // 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 ) {
+ // in subclasses, return the number of items enqueued
+ return 0;
+ }
+
+ 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( json_encode( $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 ) {
+ global $wpdb;
+
+ if ( ! $where_sql ) {
+ $where_sql = '1 = 1';
+ }
+
+ $items_per_page = 1000;
+ $page = 1;
+ $chunk_count = 0;
+ $previous_id = 0;
+ $listener = Jetpack_Sync_Listener::get_instance();
+ while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} > {$previous_id} ORDER BY {$id_field} ASC LIMIT {$items_per_page}" ) ) {
+ // Request posts in groups of N for efficiency
+ $chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE );
+
+ $listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids );
+
+ $chunk_count += count( $chunked_ids );
+ $page += 1;
+ $previous_id = end( $ids );
+ }
+
+ return $chunk_count;
+ }
+
+ protected function get_metadata( $ids, $meta_type ) {
+ global $wpdb;
+ $table = _get_meta_table( $meta_type );
+ $id = $meta_type . '_id';
+ if ( ! $table ) {
+ return array();
+ }
+
+ 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 ) ) . ' )', OBJECT )
+ );
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-modules.php b/plugins/jetpack/sync/class.jetpack-sync-modules.php
new file mode 100644
index 00000000..e197c20e
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-modules.php
@@ -0,0 +1,90 @@
+<?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-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-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-protect.php';
+require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-full-sync.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_Users',
+ 'Jetpack_Sync_Module_Posts',
+ 'Jetpack_Sync_Module_Comments',
+ 'Jetpack_Sync_Module_Updates',
+ 'Jetpack_Sync_Module_Attachments',
+ 'Jetpack_Sync_Module_Meta',
+ 'Jetpack_Sync_Module_Plugins',
+ 'Jetpack_Sync_Module_Protect',
+ 'Jetpack_Sync_Module_Full_Sync',
+ );
+
+ 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 );
+
+ return array_map( array( 'Jetpack_Sync_Modules', 'initialize_module' ), $modules );
+ }
+
+ static function initialize_module( $module_name ) {
+ $module = new $module_name;
+ $module->set_defaults();
+
+ return $module;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-queue.php b/plugins/jetpack/sync/class.jetpack-sync-queue.php
new file mode 100644
index 00000000..1f120bb8
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-queue.php
@@ -0,0 +1,419 @@
+<?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;
+ }
+
+ 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 );
+ }
+
+ do_action( 'jpsq_item_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" );
+ }
+
+ do_action( 'jpsq_items_added', $rows_added );
+ }
+
+ // 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() {
+ return self::get_lag( $this->id );
+ }
+
+ static function get_lag( $id ) {
+ 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_{$id}-%"
+ ) );
+
+ if ( ! $first_item_name ) {
+ return 0;
+ }
+
+ // break apart the item name to get the timestamp
+ $matches = null;
+ if ( preg_match( '/^jpsq_' . $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
+ );
+
+ $total_memory = 0;
+ $item_ids = array();
+
+ foreach ( $items_with_size as $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 && count( $item_ids ) > 0 ) {
+ break;
+ }
+ $item_ids[] = $item_with_size->id;
+ }
+
+ $items = $this->fetch_items_by_id( $item_ids );
+
+ 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() {
+ $this->delete_checkout_id();
+ }
+
+ private function get_checkout_id() {
+ return get_transient( $this->get_checkout_transient_name() );
+ }
+
+ private function set_checkout_id( $checkout_id ) {
+ return set_transient( $this->get_checkout_transient_name(), $checkout_id, 5 * 60 ); // 5 minute timeout
+ }
+
+ private function delete_checkout_id() {
+ delete_transient( $this->get_checkout_transient_name() );
+ }
+
+ private function get_checkout_transient_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 . '-' . getmypid() . '-' . $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 fetch_items_by_id( $item_ids ) {
+ global $wpdb;
+
+ if ( count( $item_ids ) > 0 ) {
+ $sql = "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $item_ids ), '%s' ) ) . ') ORDER BY option_name ASC';
+ $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $item_ids ) );
+ $items = $wpdb->get_results( $query, OBJECT );
+ foreach ( $items as $item ) {
+ $item->value = maybe_unserialize( $item->value );
+ }
+
+ return $items;
+ } else {
+ return array();
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-sender.php b/plugins/jetpack/sync/class.jetpack-sync-sender.php
new file mode 100644
index 00000000..3793af11
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-sender.php
@@ -0,0 +1,321 @@
+<?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-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 SYNC_THROTTLE_OPTION_NAME = 'jetpack_sync_min_wait';
+ const NEXT_SYNC_TIME_OPTION_NAME = 'jetpack_next_sync_time';
+ const WPCOM_ERROR_SYNC_DELAY = 60;
+
+ private $dequeue_max_bytes;
+ private $upload_max_bytes;
+ private $upload_max_rows;
+ private $sync_wait_time;
+ private $sync_wait_threshold;
+ private $sync_queue;
+ private $full_sync_queue;
+ private $codec;
+
+ // 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() {
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ $module->init_before_send();
+ }
+ }
+
+ public function get_next_sync_time() {
+ return (double) get_option( self::NEXT_SYNC_TIME_OPTION_NAME, 0 );
+ }
+
+ public function set_next_sync_time( $time ) {
+ return update_option( self::NEXT_SYNC_TIME_OPTION_NAME, $time, true );
+ }
+
+ public function do_sync() {
+ // don't sync if importing
+ if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
+ return false;
+ }
+
+ // don't sync if we are throttled
+ if ( $this->get_next_sync_time() > microtime( true ) ) {
+ return false;
+ }
+
+ $start_time = microtime( true );
+
+ $full_sync_result = $this->do_sync_for_queue( $this->full_sync_queue );
+ $sync_result = $this->do_sync_for_queue( $this->sync_queue );
+
+ $exceeded_sync_wait_threshold = ( microtime( true ) - $start_time ) > (double) $this->get_sync_wait_threshold();
+
+ if ( is_wp_error( $full_sync_result ) || is_wp_error( $sync_result ) ) {
+ $this->set_next_sync_time( time() + self::WPCOM_ERROR_SYNC_DELAY );
+ $full_sync_result = false;
+ $sync_result = false;
+ } 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() );
+ }
+
+ // we use OR here because if either one returns true then the caller should
+ // be allowed to call do_sync again, as there may be more items
+ return $full_sync_result || $sync_result;
+ }
+
+ public function do_sync_for_queue( $queue ) {
+
+ do_action( 'jetpack_sync_before_send_queue_' . $queue->id );
+
+ if ( $queue->size() === 0 ) {
+ return false;
+ }
+
+ // 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 );
+ }
+
+ $buffer = $queue->checkout_with_memory_limit( $this->dequeue_max_bytes, $this->upload_max_rows );
+
+ if ( ! $buffer ) {
+ // buffer has no items
+ return false;
+ }
+
+ if ( is_wp_error( $buffer ) ) {
+ // another buffer is currently sending
+ return false;
+ }
+
+ $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 = $this->codec->encode( $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;
+ }
+
+ /**
+ * 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')
+ */
+ $processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->codec->name(), microtime( true ), $queue->id );
+
+ 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 $processed_item_ids;
+ }
+
+ // returning a WP_Error is a sign to the caller that we should wait a while
+ // before syncing again
+ return new WP_Error( 'server_error' );
+
+ } 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 $wp_error;
+ }
+ }
+
+ 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 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 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;
+ }
+
+ // in seconds
+ function set_sync_wait_threshold( $seconds ) {
+ $this->sync_wait_threshold = $seconds;
+ }
+
+ function get_sync_wait_threshold() {
+ return $this->sync_wait_threshold;
+ }
+
+ function set_defaults() {
+ $this->sync_queue = new Jetpack_Sync_Queue( 'sync' );
+ $this->full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' );
+ $this->codec = new Jetpack_Sync_JSON_Deflate_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_sync_wait_threshold( $settings['sync_wait_threshold'] );
+ }
+
+ function reset_data() {
+ $this->reset_sync_queue();
+
+ foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
+ $module->reset_data();
+ }
+
+ delete_option( self::SYNC_THROTTLE_OPTION_NAME );
+ delete_option( self::NEXT_SYNC_TIME_OPTION_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' );
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-server.php b/plugins/jetpack/sync/class.jetpack-sync-server.php
new file mode 100644
index 00000000..752aa666
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-server.php
@@ -0,0 +1,106 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-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_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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-settings.php b/plugins/jetpack/sync/class.jetpack-sync-settings.php
new file mode 100644
index 00000000..5e5e39db
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-settings.php
@@ -0,0 +1,130 @@
+<?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,
+ 'max_queue_size' => true,
+ 'max_queue_lag' => true,
+ 'queue_max_writes_sec' => true,
+ 'post_types_blacklist' => true,
+ 'meta_blacklist' => true,
+ 'disable' => true,
+ );
+
+ static $is_importing;
+ static $is_doing_cron;
+
+ 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 );
+ }
+
+ // specifically for the post_types blacklist, we want to include the hardcoded settings
+ if ( $setting === 'post_types_blacklist' ) {
+ $value = array_unique( array_merge( $value, Jetpack_Sync_Defaults::$blacklisted_post_types ) );
+ }
+
+ // ditto for meta blacklist
+ if ( $setting === 'meta_blacklist' ) {
+ $value = array_unique( array_merge( $value, Jetpack_Sync_Defaults::$default_blacklist_meta_keys ) );
+ }
+
+ 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 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 );
+ }
+
+ 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;
+ }
+}
diff --git a/plugins/jetpack/sync/class.jetpack-sync-users.php b/plugins/jetpack/sync/class.jetpack-sync-users.php
new file mode 100644
index 00000000..69d4e3c5
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-users.php
@@ -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( $user_roles[ $user_id ] ) ) {
+ return $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 ) );
+ }
+
+ 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();
diff --git a/plugins/jetpack/sync/class.jetpack-sync-wp-replicastore.php b/plugins/jetpack/sync/class.jetpack-sync-wp-replicastore.php
new file mode 100644
index 00000000..9b378870
--- /dev/null
+++ b/plugins/jetpack/sync/class.jetpack-sync-wp-replicastore.php
@@ -0,0 +1,702 @@
+<?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 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', "comment_approved <> 'spam'", $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' => 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' => 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' );
+ }
+ }
+
+ // 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 ) ) {
+ $taxonomies = array();
+ foreach ( $tt_ids as $tt_id ) {
+ $term = get_term_by( 'term_taxonomy_id', $tt_id );
+ $taxonomies[ $term->taxonomy ][] = $tt_id;
+ }
+ $in_tt_ids = "'" . implode( "', '", $tt_ids ) . "'";
+
+ /**
+ * 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 );
+ $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 ) {
+ 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 get_allowed_mime_types( $user_id ) {
+
+ }
+
+ public function checksum_all() {
+ return array(
+ 'posts' => $this->posts_checksum(),
+ 'comments' => $this->comments_checksum()
+ );
+ }
+
+ function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null ) {
+ 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';
+ if ( empty( $columns ) ) {
+ $columns = Jetpack_Sync_Defaults::$default_post_checksum_columns;
+ }
+ break;
+ case "comments":
+ $object_count = $this->comment_count( null, $start_id, $end_id );
+ $object_table = $wpdb->comments;
+ $id_field = 'comment_ID';
+ if ( empty( $columns ) ) {
+ $columns = Jetpack_Sync_Defaults::$default_comment_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, '1=1', $first_id, $last_id );
+
+ 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 ) {
+ global $wpdb;
+
+ // sanitize to just valid MySQL column names
+ $sanitized_columns = preg_grep ( '/^[0-9,a-z,A-Z$_]+$/i', $columns );
+ $columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $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;
+
+ }
+
+ /**
+ * 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" );
+ }
+}
diff --git a/plugins/jetpack/sync/interface.jetpack-sync-codec.php b/plugins/jetpack/sync/interface.jetpack-sync-codec.php
new file mode 100644
index 00000000..1405d90c
--- /dev/null
+++ b/plugins/jetpack/sync/interface.jetpack-sync-codec.php
@@ -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 );
+}
diff --git a/plugins/jetpack/sync/interface.jetpack-sync-replicastore.php b/plugins/jetpack/sync/interface.jetpack-sync-replicastore.php
new file mode 100644
index 00000000..a114098e
--- /dev/null
+++ b/plugins/jetpack/sync/interface.jetpack-sync-replicastore.php
@@ -0,0 +1,129 @@
+<?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 );
+
+ // 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 );
+
+ // 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 );
+
+ // 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 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 );
+}