diff options
Diffstat (limited to 'plugins/jetpack/sync')
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 ); +} |