diff options
author | Yury German <blueknight@gentoo.org> | 2019-05-22 01:01:36 -0400 |
---|---|---|
committer | Yury German <blueknight@gentoo.org> | 2019-05-22 01:01:36 -0400 |
commit | 0914c92da22824025992c368c745546e41fbeb84 (patch) | |
tree | 965f6adf3b725e56d559fe4a93eff02281499dcc /plugins/jetpack/sync | |
parent | Deleting plugins for update (diff) | |
download | blogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.tar.gz blogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.tar.bz2 blogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.zip |
Adding Plugins
Updating the following
akismet.4.1.2, google-authenticator.0.52, jetpack.7.3.1
Signed-off-by: Yury German <blueknight@gentoo.org>
Diffstat (limited to 'plugins/jetpack/sync')
36 files changed, 8967 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..9268f78e --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-actions.php @@ -0,0 +1,485 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php'; + +/** + * The role of this class is to hook the Sync subsystem into WordPress - when to listen for actions, + * when to send, when to perform a full sync, etc. + * + * It also binds the action to send data to WPCOM to Jetpack's XMLRPC client object. + */ +class Jetpack_Sync_Actions { + static $sender = null; + static $listener = null; + const DEFAULT_SYNC_CRON_INTERVAL_NAME = 'jetpack_sync_interval'; + const DEFAULT_SYNC_CRON_INTERVAL_VALUE = 300; // 5 * MINUTE_IN_SECONDS; + + static function init() { + + // everything below this point should only happen if we're a valid sync site + if ( ! self::sync_allowed() ) { + return; + } + + if ( self::sync_via_cron_allowed() ) { + self::init_sync_cron_jobs(); + } elseif ( wp_next_scheduled( 'jetpack_sync_cron' ) ) { + self::clear_sync_cron_jobs(); + } + // When importing via cron, do not sync + add_action( 'wp_cron_importer_hook', array( __CLASS__, 'set_is_importing_true' ), 1 ); + + // Sync connected user role changes to .com + require_once dirname( __FILE__ ) . '/class.jetpack-sync-users.php'; + + // publicize filter to prevent publicizing blacklisted post types + add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 ); + + /** + * Fires on every request before default loading sync listener code. + * Return false to not load sync listener code that monitors common + * WP actions to be serialized. + * + * By default this returns true for cron jobs, non-GET-requests, or requests where the + * user is logged-in. + * + * @since 4.2.0 + * + * @param bool should we load sync listener code for this request + */ + if ( apply_filters( 'jetpack_sync_listener_should_load', true ) ) { + self::initialize_listener(); + } + + add_action( 'init', array( __CLASS__, 'add_sender_shutdown' ), 90 ); + } + + static function add_sender_shutdown() { + /** + * Fires on every request before default loading sync sender code. + * Return false to not load sync sender code that serializes pending + * data and sends it to WPCOM for processing. + * + * By default this returns true for cron jobs, POST requests, admin requests, or requests + * by users who can manage_options. + * + * @since 4.2.0 + * + * @param bool should we load sync sender code for this request + */ + if ( apply_filters( + 'jetpack_sync_sender_should_load', + ( + ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) + || + current_user_can( 'manage_options' ) + || + is_admin() + || + defined( 'PHPUNIT_JETPACK_TESTSUITE' ) + ) + ) ) { + self::initialize_sender(); + add_action( 'shutdown', array( self::$sender, 'do_sync' ) ); + add_action( 'shutdown', array( self::$sender, 'do_full_sync' ) ); + } + } + + static function sync_allowed() { + if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) { + return true; + } + require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + if ( ! Jetpack_Sync_Settings::is_sync_enabled() ) { + return false; + } + if ( Jetpack::is_development_mode() ) { + return false; + } + if ( Jetpack::is_staging_site() ) { + return false; + } + if ( ! Jetpack::is_active() ) { + if ( ! doing_action( 'jetpack_user_authorized' ) ) { + return false; + } + } + + return true; + } + + static function sync_via_cron_allowed() { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + return ( Jetpack_Sync_Settings::get_setting( 'sync_via_cron' ) ); + } + + static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + if ( in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) { + return false; + } + + return $should_publicize; + } + + static function set_is_importing_true() { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + Jetpack_Sync_Settings::set_importing( true ); + } + + static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-functions.php'; + Jetpack::load_xml_rpc_client(); + + $query_args = array( + 'sync' => '1', // add an extra parameter to the URL so we can tell it's a sync action + 'codec' => $codec_name, // send the name of the codec used to encode the data + 'timestamp' => $sent_timestamp, // send current server time so we can compensate for clock differences + 'queue' => $queue_id, // sync or full_sync + 'home' => Jetpack_Sync_Functions::home_url(), // Send home url option to check for Identity Crisis server-side + 'siteurl' => Jetpack_Sync_Functions::site_url(), // Send siteurl option to check for Identity Crisis server-side + 'cd' => sprintf( '%.4f', $checkout_duration ), // Time spent retrieving queue items from the DB + 'pd' => sprintf( '%.4f', $preprocess_duration ), // Time spent converting queue items into data to send + ); + + // Has the site opted in to IDC mitigation? + if ( Jetpack::sync_idc_optin() ) { + $query_args['idc'] = true; + } + + if ( Jetpack_Options::get_option( 'migrate_for_idc', false ) ) { + $query_args['migrate_for_idc'] = true; + } + + $query_args['timeout'] = Jetpack_Sync_Settings::is_doing_cron() ? 30 : 15; + + /** + * Filters query parameters appended to the Sync request URL sent to WordPress.com. + * + * @since 4.7.0 + * + * @param array $query_args associative array of query parameters. + */ + $query_args = apply_filters( 'jetpack_sync_send_data_query_args', $query_args ); + + $url = add_query_arg( $query_args, Jetpack::xmlrpc_api_url() ); + + $rpc = new Jetpack_IXR_Client( + array( + 'url' => $url, + 'user_id' => JETPACK_MASTER_USER, + 'timeout' => $query_args['timeout'], + ) + ); + + $result = $rpc->query( 'jetpack.syncActions', $data ); + + if ( ! $result ) { + return $rpc->get_jetpack_error(); + } + + $response = $rpc->getResponse(); + + // Check if WordPress.com IDC mitigation blocked the sync request + if ( is_array( $response ) && isset( $response['error_code'] ) ) { + $error_code = $response['error_code']; + $allowed_idc_error_codes = array( + 'jetpack_url_mismatch', + 'jetpack_home_url_mismatch', + 'jetpack_site_url_mismatch', + ); + + if ( in_array( $error_code, $allowed_idc_error_codes ) ) { + Jetpack_Options::update_option( + 'sync_error_idc', + Jetpack::get_sync_error_idc_option( $response ) + ); + } + + return new WP_Error( + 'sync_error_idc', + esc_html__( 'Sync has been blocked from WordPress.com because it would cause an identity crisis', 'jetpack' ) + ); + } + + return $response; + } + + static function do_initial_sync() { + // Lets not sync if we are not suppose to. + if ( ! self::sync_allowed() ) { + return false; + } + + $initial_sync_config = array( + 'options' => true, + 'functions' => true, + 'constants' => true, + 'users' => array( get_current_user_id() ), + ); + + if ( is_multisite() ) { + $initial_sync_config['network_options'] = true; + } + + self::do_full_sync( $initial_sync_config ); + } + + static function do_full_sync( $modules = null ) { + if ( ! self::sync_allowed() ) { + return false; + } + + $full_sync_module = Jetpack_Sync_Modules::get_module( 'full-sync' ); + + if ( ! $full_sync_module ) { + return false; + } + + self::initialize_listener(); + + $full_sync_module->start( $modules ); + + return true; + } + + static function jetpack_cron_schedule( $schedules ) { + if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) { + $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array( + 'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE, + 'display' => sprintf( + esc_html( _n( 'Every minute', 'Every %d minutes', intval( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 ), 'jetpack' ) ), + intval( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 ) + ), + ); + } + return $schedules; + } + + static function do_cron_sync() { + self::do_cron_sync_by_type( 'sync' ); + } + + static function do_cron_full_sync() { + self::do_cron_sync_by_type( 'full_sync' ); + } + + /** + * Try to send actions until we run out of things to send, + * or have to wait more than 15s before sending again, + * or we hit a lock or some other sending issue + * + * @param string $type Sync type. Can be `sync` or `full_sync`. + */ + static function do_cron_sync_by_type( $type ) { + if ( ! self::sync_allowed() || ( 'sync' !== $type && 'full_sync' !== $type ) ) { + return; + } + + self::initialize_sender(); + + $time_limit = Jetpack_Sync_Settings::get_setting( 'cron_sync_time_limit' ); + $start_time = time(); + + do { + $next_sync_time = self::$sender->get_next_sync_time( $type ); + + if ( $next_sync_time ) { + $delay = $next_sync_time - time() + 1; + if ( $delay > 15 ) { + break; + } elseif ( $delay > 0 ) { + sleep( $delay ); + } + } + + $result = 'full_sync' === $type ? self::$sender->do_full_sync() : self::$sender->do_sync(); + } while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() ); + } + + static function initialize_listener() { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php'; + self::$listener = Jetpack_Sync_Listener::get_instance(); + } + + static function initialize_sender() { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-sender.php'; + self::$sender = Jetpack_Sync_Sender::get_instance(); + + // bind the sending process + add_filter( 'jetpack_sync_send_data', array( __CLASS__, 'send_data' ), 10, 6 ); + } + + static function initialize_woocommerce() { + if ( false === class_exists( 'WooCommerce' ) ) { + return; + } + add_filter( 'jetpack_sync_modules', array( 'Jetpack_Sync_Actions', 'add_woocommerce_sync_module' ) ); + } + + static function add_woocommerce_sync_module( $sync_modules ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-woocommerce.php'; + $sync_modules[] = 'Jetpack_Sync_Module_WooCommerce'; + return $sync_modules; + } + + static function initialize_wp_super_cache() { + if ( false === function_exists( 'wp_cache_is_enabled' ) ) { + return; + } + add_filter( 'jetpack_sync_modules', array( 'Jetpack_Sync_Actions', 'add_wp_super_cache_sync_module' ) ); + } + + static function add_wp_super_cache_sync_module( $sync_modules ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-wp-super-cache.php'; + $sync_modules[] = 'Jetpack_Sync_Module_WP_Super_Cache'; + return $sync_modules; + } + + static function sanitize_filtered_sync_cron_schedule( $schedule ) { + $schedule = sanitize_key( $schedule ); + $schedules = wp_get_schedules(); + + // Make sure that the schedule has actually been registered using the `cron_intervals` filter. + if ( isset( $schedules[ $schedule ] ) ) { + return $schedule; + } + + return self::DEFAULT_SYNC_CRON_INTERVAL_NAME; + } + + static function get_start_time_offset( $schedule = '', $hook = '' ) { + $start_time_offset = is_multisite() + ? mt_rand( 0, ( 2 * self::DEFAULT_SYNC_CRON_INTERVAL_VALUE ) ) + : 0; + + /** + * Allows overriding the offset that the sync cron jobs will first run. This can be useful when scheduling + * cron jobs across multiple sites in a network. + * + * @since 4.5.0 + * + * @param int $start_time_offset + * @param string $hook + * @param string $schedule + */ + return intval( + apply_filters( + 'jetpack_sync_cron_start_time_offset', + $start_time_offset, + $hook, + $schedule + ) + ); + } + + static function maybe_schedule_sync_cron( $schedule, $hook ) { + if ( ! $hook ) { + return; + } + $schedule = self::sanitize_filtered_sync_cron_schedule( $schedule ); + + $start_time = time() + self::get_start_time_offset( $schedule, $hook ); + if ( ! wp_next_scheduled( $hook ) ) { + // Schedule a job to send pending queue items once a minute + wp_schedule_event( $start_time, $schedule, $hook ); + } elseif ( $schedule != wp_get_schedule( $hook ) ) { + // If the schedule has changed, update the schedule + wp_clear_scheduled_hook( $hook ); + wp_schedule_event( $start_time, $schedule, $hook ); + } + } + + static function clear_sync_cron_jobs() { + wp_clear_scheduled_hook( 'jetpack_sync_cron' ); + wp_clear_scheduled_hook( 'jetpack_sync_full_cron' ); + } + + static function init_sync_cron_jobs() { + add_filter( 'cron_schedules', array( __CLASS__, 'jetpack_cron_schedule' ) ); + + add_action( 'jetpack_sync_cron', array( __CLASS__, 'do_cron_sync' ) ); + add_action( 'jetpack_sync_full_cron', array( __CLASS__, 'do_cron_full_sync' ) ); + + /** + * Allows overriding of the default incremental sync cron schedule which defaults to once every 5 minutes. + * + * @since 4.3.2 + * + * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME + */ + $incremental_sync_cron_schedule = apply_filters( 'jetpack_sync_incremental_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME ); + self::maybe_schedule_sync_cron( $incremental_sync_cron_schedule, 'jetpack_sync_cron' ); + + /** + * Allows overriding of the full sync cron schedule which defaults to once every 5 minutes. + * + * @since 4.3.2 + * + * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME + */ + $full_sync_cron_schedule = apply_filters( 'jetpack_sync_full_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME ); + self::maybe_schedule_sync_cron( $full_sync_cron_schedule, 'jetpack_sync_full_cron' ); + } + + static function cleanup_on_upgrade( $new_version = null, $old_version = null ) { + if ( wp_next_scheduled( 'jetpack_sync_send_db_checksum' ) ) { + wp_clear_scheduled_hook( 'jetpack_sync_send_db_checksum' ); + } + + $is_new_sync_upgrade = version_compare( $old_version, '4.2', '>=' ); + if ( ! empty( $old_version ) && $is_new_sync_upgrade && version_compare( $old_version, '4.5', '<' ) ) { + require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + self::clear_sync_cron_jobs(); + Jetpack_Sync_Settings::update_settings( + array( + 'render_filtered_content' => Jetpack_Sync_Defaults::$default_render_filtered_content, + ) + ); + } + } + + static function get_sync_status() { + self::initialize_sender(); + + $sync_module = Jetpack_Sync_Modules::get_module( 'full-sync' ); + $queue = self::$sender->get_sync_queue(); + $full_queue = self::$sender->get_full_sync_queue(); + $cron_timestamps = array_keys( _get_cron_array() ); + $next_cron = $cron_timestamps[0] - time(); + + $full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array(); + return array_merge( + $full_sync_status, + array( + 'cron_size' => count( $cron_timestamps ), + 'next_cron' => $next_cron, + 'queue_size' => $queue->size(), + 'queue_lag' => $queue->lag(), + 'queue_next_sync' => ( self::$sender->get_next_sync_time( 'sync' ) - microtime( true ) ), + 'full_queue_size' => $full_queue->size(), + 'full_queue_lag' => $full_queue->lag(), + 'full_queue_next_sync' => ( self::$sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ), + ) + ); + } +} + +// Check for WooCommerce support +add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'initialize_woocommerce' ), 5 ); + +// Check for WP Super Cache +add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'initialize_wp_super_cache' ), 5 ); + +/* + * Init after plugins loaded and before the `init` action. This helps with issues where plugins init + * with a high priority or sites that use alternate cron. + */ +add_action( 'plugins_loaded', array( 'Jetpack_Sync_Actions', 'init' ), 90 ); + + + +// We need to define this here so that it's hooked before `updating_jetpack_version` is called +add_action( 'updating_jetpack_version', array( 'Jetpack_Sync_Actions', 'cleanup_on_upgrade' ), 10, 2 ); +add_action( 'jetpack_user_authorized', array( 'Jetpack_Sync_Actions', 'do_initial_sync' ), 10, 0 ); + 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..787145c8 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-defaults.php @@ -0,0 +1,637 @@ +<?php +require_once JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php'; + +/** + * Just some defaults that we share with the server + */ +class Jetpack_Sync_Defaults { + + static $default_options_whitelist = array( + 'stylesheet', + 'blogname', + 'blogdescription', + 'blog_charset', + 'permalink_structure', + 'category_base', + 'tag_base', + 'sidebars_widgets', + 'comment_moderation', + 'default_comment_status', + 'page_on_front', + 'rss_use_excerpt', + 'subscription_options', + 'stb_enabled', + 'stc_enabled', + 'comment_registration', + 'show_avatars', + 'avatar_default', + 'avatar_rating', + 'highlander_comment_form_prompt', + 'jetpack_comment_form_color_scheme', + 'stats_options', + 'gmt_offset', + 'timezone_string', + 'jetpack_sync_non_public_post_stati', + 'jetpack_options', + 'site_icon', // (int) - ID of core's Site Icon attachment ID + 'default_post_format', + 'default_category', + 'large_size_w', + 'large_size_h', + 'thumbnail_size_w', + 'thumbnail_size_h', + 'medium_size_w', + 'medium_size_h', + 'thumbnail_crop', + 'image_default_link_type', + 'site_logo', + 'sharing-options', + 'sharing-services', + 'post_count', + 'default_ping_status', + 'sticky_posts', + 'blog_public', + 'default_pingback_flag', + 'require_name_email', + 'close_comments_for_old_posts', + 'close_comments_days_old', + 'thread_comments', + 'thread_comments_depth', + 'page_comments', + 'comments_per_page', + 'default_comments_page', + 'comment_order', + 'comments_notify', + 'moderation_notify', + 'social_notifications_like', + 'social_notifications_reblog', + 'social_notifications_subscribe', + 'comment_whitelist', + 'comment_max_links', + 'moderation_keys', + 'jetpack_wga', + 'disabled_likes', + 'disabled_reblogs', + 'jetpack_comment_likes_enabled', + 'twitter_via', + 'jetpack-memberships-connected-account-id', + 'jetpack-twitter-cards-site-tag', + 'wpcom_publish_posts_with_markdown', + 'wpcom_publish_comments_with_markdown', + 'jetpack_activated', + 'jetpack_available_modules', + 'jetpack_autoupdate_plugins', + 'jetpack_autoupdate_plugins_translations', + 'jetpack_autoupdate_themes', + 'jetpack_autoupdate_themes_translations', + 'jetpack_autoupdate_core', + 'jetpack_autoupdate_translations', + 'carousel_background_color', + 'carousel_display_exif', + 'jetpack_portfolio', + 'jetpack_portfolio_posts_per_page', + 'jetpack_testimonial', + 'jetpack_testimonial_posts_per_page', + 'tiled_galleries', + 'gravatar_disable_hovercards', + 'infinite_scroll', + 'infinite_scroll_google_analytics', + 'wp_mobile_excerpt', + 'wp_mobile_featured_images', + 'wp_mobile_app_promos', + 'monitor_receive_notifications', + 'post_by_email_address', + 'jetpack_mailchimp', + 'jetpack_protect_key', + 'jetpack_protect_global_whitelist', + 'jetpack_sso_require_two_step', + 'jetpack_sso_match_by_email', + 'jetpack_relatedposts', + 'verification_services_codes', + 'users_can_register', + 'active_plugins', + 'uninstall_plugins', + 'advanced_seo_front_page_description', // Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION + 'advanced_seo_title_formats', // Jetpack_SEO_Titles::TITLE_FORMATS_OPTION + 'jetpack_api_cache_enabled', + 'start_of_week', + 'blacklist_keys', + 'posts_per_page', + 'posts_per_rss', + 'show_on_front', + 'ping_sites', + 'uploads_use_yearmonth_folders', + 'date_format', + 'time_format', + 'admin_email', + 'new_admin_email', + 'default_email_category', + 'default_role', + 'page_for_posts', + 'mailserver_url', + 'mailserver_login', // Not syncing contents, only the option name + 'mailserver_pass', // Not syncing contents, only the option name + 'mailserver_port', + 'wp_page_for_privacy_policy', + 'enable_header_ad', + 'wordads_second_belowpost', + 'wordads_display_front_page', + 'wordads_display_post', + 'wordads_display_page', + 'wordads_display_archive', + 'wordads_custom_adstxt', + 'site_segment', + 'site_user_type', + 'site_vertical', + 'jetpack_excluded_extensions', + ); + + public static function get_options_whitelist() { + /** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */ + $options_whitelist = apply_filters( 'jetpack_options_whitelist', self::$default_options_whitelist ); + /** + * Filter the list of WordPress options that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of options. + */ + return apply_filters( 'jetpack_sync_options_whitelist', $options_whitelist ); + } + + // Do not sync contents for these events, only the option name + static $default_options_contentless = array( + 'mailserver_login', + 'mailserver_pass', + ); + + public static function get_options_contentless() { + /** + * Filter the list of WordPress options that should be synced without content + * + * @module sync + * + * @since 6.1.0 + * + * @param array The list of options synced without content. + */ + return apply_filters( 'jetpack_sync_options_contentless', self::$default_options_contentless ); + } + + static $default_constants_whitelist = array( + 'EMPTY_TRASH_DAYS', + 'WP_POST_REVISIONS', + 'AUTOMATIC_UPDATER_DISABLED', + 'ABSPATH', + 'WP_CONTENT_DIR', + 'FS_METHOD', + 'DISALLOW_FILE_EDIT', + 'DISALLOW_FILE_MODS', + 'WP_AUTO_UPDATE_CORE', + 'WP_HTTP_BLOCK_EXTERNAL', + 'WP_ACCESSIBLE_HOSTS', + 'JETPACK__VERSION', + 'IS_PRESSABLE', + 'DISABLE_WP_CRON', + 'ALTERNATE_WP_CRON', + 'WP_CRON_LOCK_TIMEOUT', + 'PHP_VERSION', + 'WP_MEMORY_LIMIT', + 'WP_MAX_MEMORY_LIMIT', + ); + + public static function get_constants_whitelist() { + /** + * Filter the list of PHP constants that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of constants options. + */ + return apply_filters( 'jetpack_sync_constants_whitelist', self::$default_constants_whitelist ); + } + + static $default_callable_whitelist = array( + 'wp_max_upload_size' => 'wp_max_upload_size', + 'is_main_network' => array( 'Jetpack', 'is_multi_network' ), + 'is_multi_site' => 'is_multisite', + 'main_network_site' => array( 'Jetpack_Sync_Functions', 'main_network_site_url' ), + 'site_url' => array( 'Jetpack_Sync_Functions', 'site_url' ), + 'home_url' => array( 'Jetpack_Sync_Functions', 'home_url' ), + 'single_user_site' => array( 'Jetpack', 'is_single_user_site' ), + 'updates' => array( 'Jetpack', 'get_updates' ), + 'has_file_system_write_access' => array( 'Jetpack_Sync_Functions', 'file_system_write_access' ), + 'is_version_controlled' => array( 'Jetpack_Sync_Functions', 'is_version_controlled' ), + 'taxonomies' => array( 'Jetpack_Sync_Functions', 'get_taxonomies' ), + 'post_types' => array( 'Jetpack_Sync_Functions', 'get_post_types' ), + 'post_type_features' => array( 'Jetpack_Sync_Functions', 'get_post_type_features' ), + 'shortcodes' => array( 'Jetpack_Sync_Functions', 'get_shortcodes' ), + 'rest_api_allowed_post_types' => array( 'Jetpack_Sync_Functions', 'rest_api_allowed_post_types' ), + 'rest_api_allowed_public_metadata' => array( 'Jetpack_Sync_Functions', 'rest_api_allowed_public_metadata' ), + 'sso_is_two_step_required' => array( 'Jetpack_SSO_Helpers', 'is_two_step_required' ), + 'sso_should_hide_login_form' => array( 'Jetpack_SSO_Helpers', 'should_hide_login_form' ), + 'sso_match_by_email' => array( 'Jetpack_SSO_Helpers', 'match_by_email' ), + 'sso_new_user_override' => array( 'Jetpack_SSO_Helpers', 'new_user_override' ), + 'sso_bypass_default_login_form' => array( 'Jetpack_SSO_Helpers', 'bypass_login_forward_wpcom' ), + 'wp_version' => array( 'Jetpack_Sync_Functions', 'wp_version' ), + 'get_plugins' => array( 'Jetpack_Sync_Functions', 'get_plugins' ), + 'get_plugins_action_links' => array( 'Jetpack_Sync_functions', 'get_plugins_action_links' ), + 'active_modules' => array( 'Jetpack', 'get_active_modules' ), + 'hosting_provider' => array( 'Jetpack_Sync_Functions', 'get_hosting_provider' ), + 'locale' => 'get_locale', + 'site_icon_url' => array( 'Jetpack_Sync_Functions', 'site_icon_url' ), + 'roles' => array( 'Jetpack_Sync_Functions', 'roles' ), + 'timezone' => array( 'Jetpack_Sync_Functions', 'get_timezone' ), + 'available_jetpack_blocks' => array( 'Jetpack_Gutenberg', 'get_availability' ), // Includes both Gutenberg blocks *and* plugins + 'paused_themes' => array( 'Jetpack_Sync_Functions', 'get_paused_themes' ), + 'paused_plugins' => array( 'Jetpack_Sync_Functions', 'get_paused_plugins' ), + ); + + + static $default_post_type_attributes = array( + 'name' => '', + 'label' => '', + 'labels' => array(), + 'description' => '', + 'public' => false, + 'hierarchical' => false, + 'exclude_from_search' => true, + 'publicly_queryable' => null, + 'show_ui' => false, + 'show_in_menu' => null, + 'show_in_nav_menus' => null, + 'show_in_admin_bar' => false, + 'menu_position' => null, + 'menu_icon' => null, + 'supports' => array(), + 'capability_type' => 'post', + 'capabilities' => array(), + 'cap' => array(), + 'map_meta_cap' => true, + 'taxonomies' => array(), + 'has_archive' => false, + 'rewrite' => true, + 'query_var' => true, + 'can_export' => true, + 'delete_with_user' => null, + 'show_in_rest' => false, + 'rest_base' => false, + '_builtin' => false, + '_edit_link' => 'post.php?post=%d', + ); + + public static function get_callable_whitelist() { + /** + * Filter the list of callables that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of callables. + */ + return apply_filters( 'jetpack_sync_callable_whitelist', self::$default_callable_whitelist ); + } + + static $blacklisted_post_types = array( + 'ai1ec_event', + 'bwg_album', + 'bwg_gallery', + 'customize_changeset', // WP built-in post type for Customizer changesets + 'dn_wp_yt_log', + 'http', + 'idx_page', + 'jetpack_migration', + 'postman_sent_mail', + 'rssap-feed', + 'rssmi_feed_item', + 'secupress_log_action', + 'sg_optimizer_jobs', + 'snitch', + 'wpephpcompat_jobs', + 'wprss_feed_item', + 'wp_automatic', + 'jp_sitemap_master', + 'jp_sitemap', + 'jp_sitemap_index', + 'jp_img_sitemap', + 'jp_img_sitemap_index', + 'jp_vid_sitemap', + 'jp_vid_sitemap_index', + 'vip-legacy-redirect', + ); + + static $default_post_checksum_columns = array( + 'ID', + 'post_modified', + ); + + static $default_post_meta_checksum_columns = array( + 'meta_id', + 'meta_value', + ); + + static $default_comment_checksum_columns = array( + 'comment_ID', + 'comment_content', + ); + + static $default_comment_meta_checksum_columns = array( + 'meta_id', + 'meta_value', + ); + + static $default_option_checksum_columns = array( + 'option_name', + 'option_value', + ); + + static $default_multisite_callable_whitelist = array( + 'network_name' => array( 'Jetpack', 'network_name' ), + 'network_allow_new_registrations' => array( 'Jetpack', 'network_allow_new_registrations' ), + 'network_add_new_users' => array( 'Jetpack', 'network_add_new_users' ), + 'network_site_upload_space' => array( 'Jetpack', 'network_site_upload_space' ), + 'network_upload_file_types' => array( 'Jetpack', 'network_upload_file_types' ), + 'network_enable_administration_menus' => array( 'Jetpack', 'network_enable_administration_menus' ), + ); + + public static function get_multisite_callable_whitelist() { + /** + * Filter the list of multisite callables that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of multisite callables. + */ + return apply_filters( 'jetpack_sync_multisite_callable_whitelist', self::$default_multisite_callable_whitelist ); + } + + static $post_meta_whitelist = array( + '_feedback_akismet_values', + '_feedback_email', + '_feedback_extra_fields', + '_g_feedback_shortcode', + '_jetpack_post_thumbnail', + '_menu_item_classes', + '_menu_item_menu_item_parent', + '_menu_item_object', + '_menu_item_object_id', + '_menu_item_orphaned', + '_menu_item_type', + '_menu_item_xfn', + '_publicize_facebook_user', + '_publicize_twitter_user', + '_thumbnail_id', + '_wp_attached_file', + '_wp_attachment_backup_sizes', + '_wp_attachment_context', + '_wp_attachment_image_alt', + '_wp_attachment_is_custom_background', + '_wp_attachment_is_custom_header', + '_wp_attachment_metadata', + '_wp_page_template', + '_wp_trash_meta_comments_status', + '_wpas_mess', + 'content_width', + 'custom_css_add', + 'custom_css_preprocessor', + 'enclosure', + 'imagedata', + 'nova_price', + 'publicize_results', + 'sharing_disabled', + 'switch_like_status', + 'videopress_guid', + 'vimeo_poster_image', + 'advanced_seo_description', // Jetpack_SEO_Posts::DESCRIPTION_META_KEY + ); + + public static function get_post_meta_whitelist() { + /** + * Filter the list of post meta data that are manageable via the JSON API. + * + * @module sync + * + * @since 4.8.0 + * + * @param array The default list of meta data keys. + */ + return apply_filters( 'jetpack_sync_post_meta_whitelist', self::$post_meta_whitelist ); + } + + static $comment_meta_whitelist = array( + 'hc_avatar', + 'hc_post_as', + 'hc_wpcom_id_sig', + 'hc_foreign_user_id', + ); + + public static function get_comment_meta_whitelist() { + /** + * Filter the list of comment meta data that are manageable via the JSON API. + * + * @module sync + * + * @since 5.7.0 + * + * @param array The default list of comment meta data keys. + */ + return apply_filters( 'jetpack_sync_comment_meta_whitelist', self::$comment_meta_whitelist ); + } + + // TODO: move this to server? - these are theme support values + // that should be synced as jetpack_current_theme_supports_foo option values + static $default_theme_support_whitelist = array( + 'post-thumbnails', + 'post-formats', + 'custom-header', + 'custom-background', + 'custom-logo', + 'menus', + 'automatic-feed-links', + 'editor-style', + 'widgets', + 'html5', + 'title-tag', + 'jetpack-social-menu', + 'jetpack-responsive-videos', + 'infinite-scroll', + 'site-logo', + ); + + static function is_whitelisted_option( $option ) { + $whitelisted_options = self::get_options_whitelist(); + foreach ( $whitelisted_options as $whitelisted_option ) { + if ( $whitelisted_option[0] === '/' && preg_match( $whitelisted_option, $option ) ) { + return true; + } elseif ( $whitelisted_option === $option ) { + return true; + } + } + + return false; + } + + static $default_capabilities_whitelist = array( + 'switch_themes', + 'edit_themes', + 'edit_theme_options', + 'install_themes', + 'activate_plugins', + 'edit_plugins', + 'install_plugins', + 'edit_users', + 'edit_files', + 'manage_options', + 'moderate_comments', + 'manage_categories', + 'manage_links', + 'upload_files', + 'import', + 'unfiltered_html', + 'edit_posts', + 'edit_others_posts', + 'edit_published_posts', + 'publish_posts', + 'edit_pages', + 'read', + 'publish_pages', + 'edit_others_pages', + 'edit_published_pages', + 'delete_pages', + 'delete_others_pages', + 'delete_published_pages', + 'delete_posts', + 'delete_others_posts', + 'delete_published_posts', + 'delete_private_posts', + 'edit_private_posts', + 'read_private_posts', + 'delete_private_pages', + 'edit_private_pages', + 'read_private_pages', + 'delete_users', + 'create_users', + 'unfiltered_upload', + 'edit_dashboard', + 'customize', + 'delete_site', + 'update_plugins', + 'delete_plugins', + 'update_themes', + 'update_core', + 'list_users', + 'remove_users', + 'add_users', + 'promote_users', + 'delete_themes', + 'export', + 'edit_comment', + 'upload_plugins', + 'upload_themes', + ); + + public static function get_capabilities_whitelist() { + /** + * Filter the list of capabilities that we care about + * + * @module sync + * + * @since 5.5.0 + * + * @param array The default list of capabilities. + */ + return apply_filters( 'jetpack_sync_capabilities_whitelist', self::$default_capabilities_whitelist ); + } + + static function get_max_sync_execution_time() { + $max_exec_time = intval( ini_get( 'max_execution_time' ) ); + if ( 0 === $max_exec_time ) { + // 0 actually means "unlimited", but let's not treat it that way + $max_exec_time = 60; + } + return floor( $max_exec_time / 3 ); + } + + static function get_default_setting( $setting ) { + $default_name = "default_$setting"; // e.g. default_dequeue_max_bytes + return Jetpack_Sync_Defaults::$$default_name; + } + + static $default_network_options_whitelist = array( + 'site_name', + 'jetpack_protect_key', + 'jetpack_protect_global_whitelist', + 'active_sitewide_plugins', + ); + + /** + * A mapping of known importers to friendly names. + * + * Keys are the class name of the known importer. + * Values are the friendly name. + * + * @since 7.3.0 + * + * @var array + */ + public static $default_known_importers = array( + 'Blogger_Importer' => 'blogger', + 'LJ_API_Import' => 'livejournal', + 'MT_Import' => 'mt', + 'RSS_Import' => 'rss', + 'WC_Tax_Rate_Importer' => 'woo-tax-rate', + 'WP_Import' => 'wordpress', + ); + + /** + * Returns a list of known importers. + * + * @since 7.3.0 + * + * @return array Known importers with importer class names as keys and friendly names as values. + */ + public static function get_known_importers() { + /** + * Filter the list of known importers. + * + * @module sync + * + * @since 7.3.0 + * + * @param array The default list of known importers. + */ + return apply_filters( 'jetpack_sync_known_importers', self::$default_known_importers ); + } + + static $default_taxonomy_whitelist = array(); + static $default_dequeue_max_bytes = 500000; // very conservative value, 1/2 MB + static $default_upload_max_bytes = 600000; // a little bigger than the upload limit to account for serialization + static $default_upload_max_rows = 500; + static $default_sync_wait_time = 10; // seconds, between syncs + static $default_sync_wait_threshold = 5; // only wait before next send if the current send took more than X seconds + static $default_enqueue_wait_time = 10; // wait between attempting to continue a full sync, via requests + static $default_max_queue_size = 1000; + static $default_max_queue_lag = 900; // 15 minutes + static $default_queue_max_writes_sec = 100; // 100 rows a second + static $default_post_types_blacklist = array(); + static $default_post_meta_whitelist = array(); + static $default_comment_meta_whitelist = array(); + static $default_disable = 0; // completely disable sending data to wpcom + static $default_network_disable = 0; // completely disable sending data to wpcom network wide + static $default_sync_via_cron = 1; // use cron to sync + static $default_render_filtered_content = 0; // render post_filtered_content + static $default_max_enqueue_full_sync = 100; // max number of items to enqueue at a time when running full sync + static $default_max_queue_size_full_sync = 1000; // max number of total items in the full sync queue + static $default_sync_callables_wait_time = MINUTE_IN_SECONDS; // seconds before sending callables again + static $default_sync_constants_wait_time = HOUR_IN_SECONDS; // seconds before sending constants again + static $default_sync_queue_lock_timeout = 120; // 2 minutes + static $default_cron_sync_time_limit = 30; // 30 seconds + +} 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..609797bb --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-functions.php @@ -0,0 +1,403 @@ +<?php + +/* + * Utility functions to generate data synced to wpcom + */ + +class Jetpack_Sync_Functions { + const HTTPS_CHECK_OPTION_PREFIX = 'jetpack_sync_https_history_'; + const HTTPS_CHECK_HISTORY = 5; + + public static function get_modules() { + require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; + + return Jetpack_Admin::init()->get_modules(); + } + + public static function get_taxonomies() { + global $wp_taxonomies; + $wp_taxonomies_without_callbacks = array(); + foreach ( $wp_taxonomies as $taxonomy_name => $taxonomy ) { + $sanitized_taxonomy = self::sanitize_taxonomy( $taxonomy ); + if ( ! empty( $sanitized_taxonomy ) ) { + $wp_taxonomies_without_callbacks[ $taxonomy_name ] = $sanitized_taxonomy; + } else { + error_log( 'Jetpack: Encountered a recusive taxonomy:' . $taxonomy_name ); + } + } + return $wp_taxonomies_without_callbacks; + } + + public static function get_shortcodes() { + global $shortcode_tags; + return array_keys( $shortcode_tags ); + } + + /** + * Removes any callback data since we will not be able to process it on our side anyways. + */ + public static function sanitize_taxonomy( $taxonomy ) { + + // Lets clone the taxonomy object instead of modifing the global one. + $cloned_taxonomy = json_decode( wp_json_encode( $taxonomy ) ); + + // recursive taxonomies are no fun. + if ( is_null( $cloned_taxonomy ) ) { + return null; + } + // Remove any meta_box_cb if they are not the default wp ones. + if ( isset( $cloned_taxonomy->meta_box_cb ) && + ! in_array( $cloned_taxonomy->meta_box_cb, array( 'post_tags_meta_box', 'post_categories_meta_box' ) ) ) { + $cloned_taxonomy->meta_box_cb = null; + } + // Remove update call back + if ( isset( $cloned_taxonomy->update_count_callback ) && + ! is_null( $cloned_taxonomy->update_count_callback ) ) { + $cloned_taxonomy->update_count_callback = null; + } + // Remove rest_controller_class if it something other then the default. + if ( isset( $cloned_taxonomy->rest_controller_class ) && + 'WP_REST_Terms_Controller' !== $cloned_taxonomy->rest_controller_class ) { + $cloned_taxonomy->rest_controller_class = null; + } + return $cloned_taxonomy; + } + + public static function get_post_types() { + global $wp_post_types; + + $post_types_without_callbacks = array(); + foreach ( $wp_post_types as $post_type_name => $post_type ) { + $sanitized_post_type = self::sanitize_post_type( $post_type ); + if ( ! empty( $sanitized_post_type ) ) { + $post_types_without_callbacks[ $post_type_name ] = $sanitized_post_type; + } else { + error_log( 'Jetpack: Encountered a recusive post_type:' . $post_type_name ); + } + } + return $post_types_without_callbacks; + } + + public static function sanitize_post_type( $post_type ) { + // Lets clone the post type object instead of modifing the global one. + $sanitized_post_type = array(); + foreach ( Jetpack_Sync_Defaults::$default_post_type_attributes as $attribute_key => $default_value ) { + if ( isset( $post_type->{ $attribute_key } ) ) { + $sanitized_post_type[ $attribute_key ] = $post_type->{ $attribute_key }; + } + } + return (object) $sanitized_post_type; + } + + public static function expand_synced_post_type( $sanitized_post_type, $post_type ) { + $post_type = sanitize_key( $post_type ); + $post_type_object = new WP_Post_Type( $post_type, $sanitized_post_type ); + $post_type_object->add_supports(); + $post_type_object->add_rewrite_rules(); + $post_type_object->add_hooks(); + $post_type_object->register_taxonomies(); + return (object) $post_type_object; + } + + public static function get_post_type_features() { + global $_wp_post_type_features; + + return $_wp_post_type_features; + } + + public static function get_hosting_provider() { + if ( defined( 'GD_SYSTEM_PLUGIN_DIR' ) || class_exists( '\\WPaaS\\Plugin' ) ) { + return 'gd-managed-wp'; + } + if ( defined( 'MM_BASE_DIR' ) ) { + return 'bh'; + } + if ( defined( 'IS_PRESSABLE' ) ) { + return 'pressable'; + } + if ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ) { + return 'wpe'; + } + if ( defined( 'VIP_GO_ENV' ) && false !== VIP_GO_ENV ) { + return 'vip-go'; + } + return 'unknown'; + } + + public static function rest_api_allowed_post_types() { + /** This filter is already documented in class.json-api-endpoints.php */ + return apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) ); + } + + public static function rest_api_allowed_public_metadata() { + /** This filter is documented in json-endpoints/class.wpcom-json-api-post-endpoint.php */ + return apply_filters( 'rest_api_allowed_public_metadata', array() ); + } + + /** + * Finds out if a site is using a version control system. + * + * @return bool + **/ + public static function is_version_controlled() { + + if ( ! class_exists( 'WP_Automatic_Updater' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + } + $updater = new WP_Automatic_Updater(); + + return (bool) strval( $updater->is_vcs_checkout( $context = ABSPATH ) ); + } + + /** + * Returns true if the site has file write access false otherwise. + * + * @return bool + **/ + public static function file_system_write_access() { + if ( ! function_exists( 'get_filesystem_method' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + require_once ABSPATH . 'wp-admin/includes/template.php'; + + $filesystem_method = get_filesystem_method(); + if ( 'direct' === $filesystem_method ) { + return true; + } + + ob_start(); + + if ( ! function_exists( 'request_filesystem_credentials' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); + ob_end_clean(); + if ( $filesystem_credentials_are_stored ) { + return true; + } + + return false; + } + + /** + * Helper function that is used when getting home or siteurl values. Decides + * whether to get the raw or filtered value. + * + * @return string + */ + public static function get_raw_or_filtered_url( $url_type ) { + $url_function = ( 'home' == $url_type ) + ? 'home_url' + : 'site_url'; + + if ( + ! Jetpack_Constants::is_defined( 'JETPACK_SYNC_USE_RAW_URL' ) || + Jetpack_Constants::get_constant( 'JETPACK_SYNC_USE_RAW_URL' ) + ) { + $scheme = is_ssl() ? 'https' : 'http'; + $url = self::get_raw_url( $url_type ); + $url = set_url_scheme( $url, $scheme ); + } else { + $url = self::normalize_www_in_url( $url_type, $url_function ); + } + + return self::get_protocol_normalized_url( $url_function, $url ); + } + + public static function home_url() { + $url = self::get_raw_or_filtered_url( 'home' ); + + /** + * Allows overriding of the home_url value that is synced back to WordPress.com. + * + * @since 5.2.0 + * + * @param string $home_url + */ + return esc_url_raw( apply_filters( 'jetpack_sync_home_url', $url ) ); + } + + public static function site_url() { + $url = self::get_raw_or_filtered_url( 'siteurl' ); + + /** + * Allows overriding of the site_url value that is synced back to WordPress.com. + * + * @since 5.2.0 + * + * @param string $site_url + */ + return esc_url_raw( apply_filters( 'jetpack_sync_site_url', $url ) ); + } + + public static function main_network_site_url() { + return self::get_protocol_normalized_url( 'main_network_site_url', network_site_url() ); + } + + public static function get_protocol_normalized_url( $callable, $new_value ) { + $option_key = self::HTTPS_CHECK_OPTION_PREFIX . $callable; + + $parsed_url = wp_parse_url( $new_value ); + if ( ! $parsed_url ) { + return $new_value; + } + if ( array_key_exists( 'scheme', $parsed_url ) ) { + $scheme = $parsed_url['scheme']; + } else { + $scheme = ''; + } + $scheme_history = get_option( $option_key, array() ); + $scheme_history[] = $scheme; + + // Limit length to self::HTTPS_CHECK_HISTORY + $scheme_history = array_slice( $scheme_history, ( self::HTTPS_CHECK_HISTORY * -1 ) ); + + update_option( $option_key, $scheme_history ); + + $forced_scheme = in_array( 'https', $scheme_history ) ? 'https' : 'http'; + + return set_url_scheme( $new_value, $forced_scheme ); + } + + public static function get_raw_url( $option_name ) { + $value = null; + $constant = ( 'home' == $option_name ) + ? 'WP_HOME' + : 'WP_SITEURL'; + + // Since we disregard the constant for multisites in ms-default-filters.php, + // let's also use the db value if this is a multisite. + if ( ! is_multisite() && Jetpack_Constants::is_defined( $constant ) ) { + $value = Jetpack_Constants::get_constant( $constant ); + } else { + // Let's get the option from the database so that we can bypass filters. This will help + // ensure that we get more uniform values. + $value = Jetpack_Options::get_raw_option( $option_name ); + } + + return $value; + } + + public static function normalize_www_in_url( $option, $url_function ) { + $url = wp_parse_url( call_user_func( $url_function ) ); + $option_url = wp_parse_url( get_option( $option ) ); + + if ( ! $option_url || ! $url ) { + return $url; + } + + if ( $url['host'] === "www.{$option_url[ 'host' ]}" ) { + // remove www if not present in option URL + $url['host'] = $option_url['host']; + } + if ( $option_url['host'] === "www.{$url[ 'host' ]}" ) { + // add www if present in option URL + $url['host'] = $option_url['host']; + } + + $normalized_url = "{$url['scheme']}://{$url['host']}"; + if ( isset( $url['path'] ) ) { + $normalized_url .= "{$url['path']}"; + } + + if ( isset( $url['query'] ) ) { + $normalized_url .= "?{$url['query']}"; + } + + return $normalized_url; + } + + public static function get_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */ + return apply_filters( 'all_plugins', get_plugins() ); + } + + /** + * Get custom action link tags that the plugin is using + * Ref: https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name) + * + * @return array of plugin action links (key: link name value: url) + */ + public static function get_plugins_action_links( $plugin_file_singular = null ) { + // Some sites may have DOM disabled in PHP fail early + if ( ! class_exists( 'DOMDocument' ) ) { + return array(); + } + $plugins_action_links = get_option( 'jetpack_plugin_api_action_links', array() ); + if ( ! empty( $plugins_action_links ) ) { + if ( is_null( $plugin_file_singular ) ) { + return $plugins_action_links; + } + return ( isset( $plugins_action_links[ $plugin_file_singular ] ) ? $plugins_action_links[ $plugin_file_singular ] : null ); + } + return array(); + } + + public static function wp_version() { + global $wp_version; + return $wp_version; + } + + public static function site_icon_url( $size = 512 ) { + $site_icon = get_site_icon_url( $size ); + return $site_icon ? $site_icon : get_option( 'jetpack_site_icon_url' ); + } + + public static function roles() { + $wp_roles = wp_roles(); + return $wp_roles->roles; + } + + /** + * Determine time zone from WordPress' options "timezone_string" + * and "gmt_offset". + * + * 1. Check if `timezone_string` is set and return it. + * 2. Check if `gmt_offset` is set, formats UTC-offset from it and return it. + * 3. Default to "UTC+0" if nothing is set. + * + * @return string + */ + public static function get_timezone() { + $timezone_string = get_option( 'timezone_string' ); + + if ( ! empty( $timezone_string ) ) { + return str_replace( '_', ' ', $timezone_string ); + } + + $gmt_offset = get_option( 'gmt_offset', 0 ); + + $formatted_gmt_offset = sprintf( '%+g', floatval( $gmt_offset ) ); + + $formatted_gmt_offset = str_replace( + array( '.25', '.5', '.75' ), + array( ':15', ':30', ':45' ), + (string) $formatted_gmt_offset + ); + + /* translators: %s is UTC offset, e.g. "+1" */ + return sprintf( __( 'UTC%s', 'jetpack' ), $formatted_gmt_offset ); + } + // New in WP 5.1 + public static function get_paused_themes() { + if ( function_exists( 'wp_paused_themes' ) ) { + $paused_themes = wp_paused_themes(); + return $paused_themes->get_all(); + } + return false; + } + // New in WP 5.1 + public static function get_paused_plugins() { + if ( function_exists( 'wp_paused_plugins' ) ) { + $paused_plugins = wp_paused_plugins(); + return $paused_plugins->get_all(); + } + return false; + } +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-json-deflate-array-codec.php b/plugins/jetpack/sync/class.jetpack-sync-json-deflate-array-codec.php new file mode 100644 index 00000000..04d3dd7d --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-json-deflate-array-codec.php @@ -0,0 +1,84 @@ +<?php + +require_once dirname( __FILE__ ) . '/interface.jetpack-sync-codec.php'; + +/** + * An implementation of iJetpack_Sync_Codec that uses gzip's DEFLATE + * algorithm to compress objects serialized using json_encode + */ +class Jetpack_Sync_JSON_Deflate_Array_Codec implements iJetpack_Sync_Codec { + const CODEC_NAME = 'deflate-json-array'; + + public function name() { + return self::CODEC_NAME; + } + + public function encode( $object ) { + return base64_encode( gzdeflate( $this->json_serialize( $object ) ) ); + } + + public function decode( $input ) { + return $this->json_unserialize( gzinflate( base64_decode( $input ) ) ); + } + + // @see https://gist.github.com/muhqu/820694 + protected function json_serialize( $any ) { + if ( function_exists( 'jetpack_json_wrap' ) ) { + return wp_json_encode( jetpack_json_wrap( $any ) ); + } + // This prevents fatal error when updating pre 6.0 via the cli command + return wp_json_encode( $this->json_wrap( $any ) ); + } + + protected function json_unserialize( $str ) { + return $this->json_unwrap( json_decode( $str, true ) ); + } + + private function json_wrap( &$any, $seen_nodes = array() ) { + if ( is_object( $any ) ) { + $input = get_object_vars( $any ); + $input['__o'] = 1; + } else { + $input = &$any; + } + + if ( is_array( $input ) ) { + $seen_nodes[] = &$any; + + $return = array(); + + foreach ( $input as $k => &$v ) { + if ( ( is_array( $v ) || is_object( $v ) ) ) { + if ( in_array( $v, $seen_nodes, true ) ) { + continue; + } + $return[ $k ] = $this->json_wrap( $v, $seen_nodes ); + } else { + $return[ $k ] = $v; + } + } + + return $return; + } + + return $any; + } + + private function json_unwrap( $any ) { + if ( is_array( $any ) ) { + foreach ( $any as $k => $v ) { + if ( '__o' === $k ) { + continue; + } + $any[ $k ] = $this->json_unwrap( $v ); + } + + if ( isset( $any['__o'] ) ) { + unset( $any['__o'] ); + $any = (object) $any; + } + } + + return $any; + } +} 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..62abc0d2 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-listener.php @@ -0,0 +1,329 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-queue.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-actions.php'; + +/** + * This class monitors actions and logs them to the queue to be sent + */ +class Jetpack_Sync_Listener { + const QUEUE_STATE_CHECK_TRANSIENT = 'jetpack_sync_last_checked_queue_state'; + const QUEUE_STATE_CHECK_TIMEOUT = 300; // 5 minutes + + private $sync_queue; + private $full_sync_queue; + private $sync_queue_size_limit; + private $sync_queue_lag_limit; + + // singleton functions + private static $instance; + + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + // this is necessary because you can't use "new" when you declare instance properties >:( + protected function __construct() { + $this->set_defaults(); + $this->init(); + } + + private function init() { + $handler = array( $this, 'action_handler' ); + $full_sync_handler = array( $this, 'full_sync_action_handler' ); + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module->init_listeners( $handler ); + $module->init_full_sync_listeners( $full_sync_handler ); + } + + // Module Activation + add_action( 'jetpack_activate_module', $handler ); + add_action( 'jetpack_deactivate_module', $handler ); + + // Jetpack Upgrade + add_action( 'updating_jetpack_version', $handler, 10, 2 ); + + // Send periodic checksum + add_action( 'jetpack_sync_checksum', $handler ); + } + + function get_sync_queue() { + return $this->sync_queue; + } + + function get_full_sync_queue() { + return $this->full_sync_queue; + } + + function set_queue_size_limit( $limit ) { + $this->sync_queue_size_limit = $limit; + } + + function get_queue_size_limit() { + return $this->sync_queue_size_limit; + } + + function set_queue_lag_limit( $age ) { + $this->sync_queue_lag_limit = $age; + } + + function get_queue_lag_limit() { + return $this->sync_queue_lag_limit; + } + + function force_recheck_queue_limit() { + delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->sync_queue->id ); + delete_transient( self::QUEUE_STATE_CHECK_TRANSIENT . '_' . $this->full_sync_queue->id ); + } + + // prevent adding items to the queue if it hasn't sent an item for 15 mins + // AND the queue is over 1000 items long (by default) + function can_add_to_queue( $queue ) { + if ( ! Jetpack_Sync_Settings::is_sync_enabled() ) { + 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 + + /** + * Bulk add action to the queue. + * + * @param $action_name String the name the full sync action. + * @param $args_array Array of chunked arguments + */ + 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 ) { + $previous_end = isset( $args['previous_end'] ) ? $args['previous_end'] : null; + $args = isset( $args['ids'] ) ? $args['ids'] : $args; + + + /** + * Modify or reject the data within an action before it is enqueued locally. + * + * @since 4.2.0 + * + * @module sync + * + * @param array The action parameters + */ + $args = apply_filters( "jetpack_sync_before_enqueue_$action_name", $args ); + $action_data = array( $args ); + if ( ! is_null( $previous_end ) ) { + $action_data[] = $previous_end; + } + // allow listeners to abort + if ( $args === false ) { + continue; + } + + $data_to_enqueue[] = array( + $action_name, + $action_data, + $user_id, + $currtime, + $is_importing, + ); + } + + $queue->add_all( $data_to_enqueue ); + } + + function enqueue_action( $current_filter, $args, $queue ) { + // don't enqueue an action during the outbound http request - this prevents recursion + if ( Jetpack_Sync_Settings::is_sending() ) { + return; + } + + /** + * Add an action hook to execute when anything on the whitelist gets sent to the queue to sync. + * + * @module sync + * + * @since 5.9.0 + */ + do_action( 'jetpack_sync_action_before_enqueue' ); + + /** + * Modify or reject the data within an action before it is enqueued locally. + * + * @since 4.2.0 + * + * @param array The action parameters + */ + $args = apply_filters( "jetpack_sync_before_enqueue_$current_filter", $args ); + + // allow listeners to abort + if ( $args === false ) { + return; + } + + // periodically check the size of the queue, and disable adding to it if + // it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped) + if ( ! $this->can_add_to_queue( $queue ) ) { + return; + } + + // if we add any items to the queue, we should try to ensure that our script + // can't be killed before they are sent + if ( function_exists( 'ignore_user_abort' ) ) { + ignore_user_abort( true ); + } + + if ( + 'sync' === $queue->id || + in_array( + $current_filter, + array( + 'jetpack_full_sync_start', + 'jetpack_full_sync_end', + 'jetpack_full_sync_cancel', + ) + ) + ) { + $queue->add( + array( + $current_filter, + $args, + get_current_user_id(), + microtime( true ), + Jetpack_Sync_Settings::is_importing(), + $this->get_actor( $current_filter, $args ), + ) + ); + } else { + $queue->add( + array( + $current_filter, + $args, + get_current_user_id(), + microtime( true ), + Jetpack_Sync_Settings::is_importing(), + ) + ); + } + + // since we've added some items, let's try to load the sender so we can send them as quickly as possible + if ( ! Jetpack_Sync_Actions::$sender ) { + add_filter( 'jetpack_sync_sender_should_load', '__return_true' ); + if ( did_action( 'init' ) ) { + Jetpack_Sync_Actions::add_sender_shutdown(); + } + } + } + + function get_actor( $current_filter, $args ) { + if ( 'wp_login' === $current_filter ) { + $user = get_user_by( 'ID', $args[1]->data->ID ); + } else { + $user = wp_get_current_user(); + } + + $translated_role = Jetpack::translate_user_to_role( $user ); + + $actor = array( + 'wpcom_user_id' => null, + 'external_user_id' => isset( $user->ID ) ? $user->ID : null, + 'display_name' => isset( $user->display_name ) ? $user->display_name : null, + 'user_email' => isset( $user->user_email ) ? $user->user_email : null, + 'user_roles' => isset( $user->roles ) ? $user->roles : null, + 'translated_role' => $translated_role ? $translated_role : null, + 'is_cron' => defined( 'DOING_CRON' ) ? DOING_CRON : false, + 'is_rest' => defined( 'REST_API_REQUEST' ) ? REST_API_REQUEST : false, + 'is_xmlrpc' => defined( 'XMLRPC_REQUEST' ) ? XMLRPC_REQUEST : false, + 'is_wp_rest' => defined( 'REST_REQUEST' ) ? REST_REQUEST : false, + 'is_ajax' => defined( 'DOING_AJAX' ) ? DOING_AJAX : false, + 'is_wp_admin' => is_admin(), + 'is_cli' => defined( 'WP_CLI' ) ? WP_CLI : false, + 'from_url' => $this->get_request_url(), + ); + + if ( $this->should_send_user_data_with_actor( $current_filter ) ) { + require_once JETPACK__PLUGIN_DIR . 'modules/protect/shared-functions.php'; + $actor['ip'] = jetpack_protect_get_ip(); + $actor['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown'; + } + + return $actor; + } + + function should_send_user_data_with_actor( $current_filter ) { + $should_send = in_array( $current_filter, array( 'jetpack_wp_login', 'wp_logout', 'jetpack_valid_failed_login_attempt' ) ); + /** + * Allow or deny sending actor's user data ( IP and UA ) during a sync event + * + * @since 5.8.0 + * + * @module sync + * + * @param bool True if we should send user data + * @param string The current filter that is performing the sync action + */ + return apply_filters( 'jetpack_sync_actor_user_data', $should_send, $current_filter ); + } + + function set_defaults() { + $this->sync_queue = new Jetpack_Sync_Queue( 'sync' ); + $this->full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' ); + $this->set_queue_size_limit( Jetpack_Sync_Settings::get_setting( 'max_queue_size' ) ); + $this->set_queue_lag_limit( Jetpack_Sync_Settings::get_setting( 'max_queue_lag' ) ); + } + + function get_request_url() { + if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + return 'http' . ( isset( $_SERVER['HTTPS'] ) ? 's' : '' ) . '://' . "{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; + } + return is_admin() ? get_admin_url( get_current_blog_id() ) : home_url(); + } +} 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..f68ba832 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-attachments.php @@ -0,0 +1,55 @@ +<?php + +class Jetpack_Sync_Module_Attachments extends Jetpack_Sync_Module { + function name() { + return 'attachments'; + } + + public function init_listeners( $callable ) { + add_action( 'add_attachment', array( $this, 'process_add' ) ); + add_action( 'attachment_updated', array( $this, 'process_update' ), 10, 3 ); + add_action( 'jetpack_sync_save_update_attachment', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_add_attachment', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_attach_attachment', $callable, 10, 2 ); + } + + function process_add( $attachment_id ) { + $attachment = get_post( $attachment_id ); + /** + * Fires when the client needs to sync an new attachment + * + * @since 4.2.0 + * + * @param int The attachment ID + * @param object The attachment + */ + do_action( 'jetpack_sync_save_add_attachment', $attachment_id, $attachment ); + } + + function process_update( $attachment_id, $attachment_after, $attachment_before ) { + // Check whether attachment was added to a post for the first time + if ( 0 === $attachment_before->post_parent && 0 !== $attachment_after->post_parent ) { + /** + * Fires when an existing attachment is added to a post for the first time + * + * @since 6.6.0 + * + * @param int The attachment ID + * @param object The attachment + */ + do_action( 'jetpack_sync_save_attach_attachment', $attachment_id, $attachment_after ); + } else { + /** + * Fires when the client needs to sync an updated attachment + * + * @since 4.9.0 + * + * @param int The attachment ID + * @param object The attachment + * + * Previously this action was synced using jetpack_sync_save_add_attachment action. + */ + do_action( 'jetpack_sync_save_update_attachment', $attachment_id, $attachment_after ); + } + } +} 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..d46d5514 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-callables.php @@ -0,0 +1,273 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-functions.php'; + +class Jetpack_Sync_Module_Callables extends Jetpack_Sync_Module { + const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum'; + const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await'; + + private $callable_whitelist; + + public function name() { + return 'functions'; + } + + public function set_defaults() { + if ( is_multisite() ) { + $this->callable_whitelist = array_merge( Jetpack_Sync_Defaults::get_callable_whitelist(), Jetpack_Sync_Defaults::get_multisite_callable_whitelist() ); + } else { + $this->callable_whitelist = Jetpack_Sync_Defaults::get_callable_whitelist(); + } + } + + public function init_listeners( $callable ) { + add_action( 'jetpack_sync_callable', $callable, 10, 2 ); + add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late + + // For some options, we should always send the change right away! + $always_send_updates_to_these_options = array( + 'jetpack_active_modules', + 'home', + 'siteurl', + 'jetpack_sync_error_idc', + 'paused_plugins', + 'paused_themes', + ); + foreach ( $always_send_updates_to_these_options as $option ) { + add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) ); + add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) ); + } + + // Provide a hook so that hosts can send changes to certain callables right away. + // Especially useful when a host uses constants to change home and siteurl. + add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) ); + + // get_plugins and wp_version + // gets fired when new code gets installed, updates etc. + add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) ); + add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_callables', $callable ); + } + + public function init_before_send() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) ); + + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) ); + } + + public function reset_data() { + delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME ); + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + + $url_callables = array( 'home_url', 'site_url', 'main_network_site_url' ); + foreach ( $url_callables as $callable ) { + delete_option( Jetpack_Sync_Functions::HTTPS_CHECK_OPTION_PREFIX . $callable ); + } + } + + function set_callable_whitelist( $callables ) { + $this->callable_whitelist = $callables; + } + + function get_callable_whitelist() { + return $this->callable_whitelist; + } + + public function get_all_callables() { + // get_all_callables should run as the master user always. + $current_user_id = get_current_user_id(); + wp_set_current_user( Jetpack_Options::get_option( 'master_user' ) ); + $callables = array_combine( + array_keys( $this->get_callable_whitelist() ), + array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) ) + ); + wp_set_current_user( $current_user_id ); + return $callables; + } + + private function get_callable( $callable ) { + return call_user_func( $callable ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + /** + * Tells the client to sync all callables to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand callables (should always be true) + */ + do_action( 'jetpack_full_sync_callables', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + public function estimate_full_sync_actions( $config ) { + return 1; + } + + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_callables' ); + } + + public function unlock_sync_callable() { + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + } + + public function unlock_plugin_action_link_and_callables() { + delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ); + delete_transient( 'jetpack_plugin_api_action_links_refresh' ); + add_filter( 'jetpack_check_and_send_callables', '__return_true' ); + } + + public function set_plugin_action_links() { + if ( + ! class_exists( 'DOMDocument' ) || + ! function_exists( 'libxml_use_internal_errors' ) || + ! function_exists( 'mb_convert_encoding' ) + ) { + return; + } + + $current_screeen = get_current_screen(); + + $plugins_action_links = array(); + // Is the transient lock in place? + $plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false ); + if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && $current_screeen->id !== 'plugins' ) ) { + return; + } + $plugins = array_keys( Jetpack_Sync_Functions::get_plugins() ); + foreach ( $plugins as $plugin_file ) { + /** + * Plugins often like to unset things but things break if they are not able to. + */ + $action_links = array( + 'deactivate' => '', + 'activate' => '', + 'details' => '', + 'delete' => '', + 'edit' => '', + ); + /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */ + $action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' ); + /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */ + $action_links = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' ); + $action_links = array_filter( $action_links ); + $formatted_action_links = null; + if ( ! empty( $action_links ) && count( $action_links ) > 0 ) { + $dom_doc = new DOMDocument(); + foreach ( $action_links as $action_link ) { + // The @ is not enough to suppress errors when dealing with libxml, + // we have to tell it directly how we want to handle errors. + libxml_use_internal_errors( true ); + $dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) ); + libxml_use_internal_errors( false ); + + $link_elements = $dom_doc->getElementsByTagName( 'a' ); + if ( $link_elements->length == 0 ) { + continue; + } + + $link_element = $link_elements->item( 0 ); + if ( $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) { + $link_url = trim( $link_element->getAttribute( 'href' ) ); + + // Add the full admin path to the url if the plugin did not provide it + $link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME ); + if ( empty( $link_url_scheme ) ) { + $link_url = admin_url( $link_url ); + } + + $formatted_action_links[ $link_element->nodeValue ] = $link_url; + } + } + } + if ( $formatted_action_links ) { + $plugins_action_links[ $plugin_file ] = $formatted_action_links; + } + } + // Cache things for a long time + set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS ); + update_option( 'jetpack_plugin_api_action_links', $plugins_action_links ); + } + + public function should_send_callable( $callable_checksums, $name, $checksum ) { + $idc_override_callables = array( + 'main_network_site', + 'home_url', + 'site_url', + ); + if ( in_array( $name, $idc_override_callables ) && Jetpack_Options::get_option( 'migrate_for_idc' ) ) { + return true; + } + + return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum ); + } + + public function maybe_sync_callables() { + if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) { + if ( ! is_admin() || Jetpack_Sync_Settings::is_doing_cron() ) { + return; + } + + if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) { + return; + } + } + + set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Jetpack_Sync_Defaults::$default_sync_callables_wait_time ); + + $callables = $this->get_all_callables(); + + if ( empty( $callables ) ) { + return; + } + + $callable_checksums = (array) Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() ); + $has_changed = false; + // only send the callables that have changed + foreach ( $callables as $name => $value ) { + $checksum = $this->get_check_sum( $value ); + // explicitly not using Identical comparison as get_option returns a string + if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) { + /** + * Tells the client to sync a callable (aka function) to the server + * + * @since 4.2.0 + * + * @param string The name of the callable + * @param mixed The value of the callable + */ + do_action( 'jetpack_sync_callable', $name, $value ); + $callable_checksums[ $name ] = $checksum; + $has_changed = true; + } else { + $callable_checksums[ $name ] = $checksum; + } + } + if ( $has_changed ) { + Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums ); + } + + } + + public function expand_callables( $args ) { + if ( $args[0] ) { + $callables = $this->get_all_callables(); + $callables_checksums = array(); + foreach ( $callables as $name => $value ) { + $callables_checksums[ $name ] = $this->get_check_sum( $value ); + } + Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums ); + return $callables; + } + + return $args; + } +} 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..0a0ee16f --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-comments.php @@ -0,0 +1,196 @@ +<?php + +class Jetpack_Sync_Module_Comments extends Jetpack_Sync_Module { + + public function name() { + return 'comments'; + } + + public function get_object_by_id( $object_type, $id ) { + $comment_id = intval( $id ); + if ( $object_type === 'comment' && $comment = get_comment( $comment_id ) ) { + return $this->filter_comment( $comment ); + } + + return false; + } + + public function init_listeners( $callable ) { + add_action( 'wp_insert_comment', $callable, 10, 2 ); + add_action( 'deleted_comment', $callable ); + add_action( 'trashed_comment', $callable ); + add_action( 'spammed_comment', $callable ); + add_action( 'trashed_post_comments', $callable, 10, 2 ); + add_action( 'untrash_post_comments', $callable ); + add_action( 'comment_approved_to_unapproved', $callable ); + add_action( 'comment_unapproved_to_approved', $callable ); + add_action( 'jetpack_modified_comment_contents', $callable, 10, 2 ); + add_action( 'untrashed_comment', $callable, 10, 2 ); + add_action( 'unspammed_comment', $callable, 10, 2 ); + add_filter( 'wp_update_comment_data', array( $this, 'handle_comment_contents_modification' ), 10, 3 ); + + // even though it's messy, we implement these hooks because + // the edit_comment hook doesn't include the data + // so this saves us a DB read for every comment event + foreach ( array( '', 'trackback', 'pingback' ) as $comment_type ) { + foreach ( array( 'unapproved', 'approved' ) as $comment_status ) { + $comment_action_name = "comment_{$comment_status}_{$comment_type}"; + add_action( $comment_action_name, $callable, 10, 2 ); + } + } + + // listen for meta changes + $this->init_listeners_for_meta_type( 'comment', $callable ); + $this->init_meta_whitelist_handler( 'comment', array( $this, 'filter_meta' ) ); + } + + public function handle_comment_contents_modification( $new_comment, $old_comment, $new_comment_with_slashes ) { + $content_fields = array( + 'comment_author', + 'comment_author_email', + 'comment_author_url', + 'comment_content', + ); + $changes = array(); + foreach ( $content_fields as $field ) { + if ( $new_comment_with_slashes[ $field ] != $old_comment[ $field ] ) { + $changes[ $field ] = array( $new_comment[ $field ], $old_comment[ $field ] ); + } + } + + if ( ! empty( $changes ) ) { + /** + * Signals to the sync listener that this comment's contents were modified and a sync action + * reflecting the change(s) to the content should be sent + * + * @since 4.9.0 + * + * @param int $new_comment['comment_ID'] ID of comment whose content was modified + * @param mixed $changes Array of changed comment fields with before and after values + */ + do_action( 'jetpack_modified_comment_contents', $new_comment['comment_ID'], $changes ); + } + return $new_comment; + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_comments', $callable ); // also send comments meta + } + + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_wp_insert_comment', array( $this, 'expand_wp_insert_comment' ) ); + + foreach ( array( '', 'trackback', 'pingback' ) as $comment_type ) { + foreach ( array( 'unapproved', 'approved' ) as $comment_status ) { + $comment_action_name = "comment_{$comment_status}_{$comment_type}"; + add_filter( + 'jetpack_sync_before_send_' . $comment_action_name, + array( + $this, + 'expand_wp_insert_comment', + ) + ); + } + } + + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->comments"; + + if ( $where_sql = $this->get_where_sql( $config ) ) { + $query .= ' WHERE ' . $where_sql; + } + + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + private function get_where_sql( $config ) { + if ( is_array( $config ) ) { + return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return null; + } + + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_comments' ); + } + + public function count_full_sync_actions( $action_names ) { + return $this->count_actions( $action_names, array( 'jetpack_full_sync_comments' ) ); + } + + function expand_wp_comment_status_change( $args ) { + return array( $args[0], $this->filter_comment( $args[1] ) ); + } + + function expand_wp_insert_comment( $args ) { + return array( $args[0], $this->filter_comment( $args[1] ) ); + } + + function filter_comment( $comment ) { + /** + * Filters whether to prevent sending comment data to .com + * + * Passing true to the filter will prevent the comment data from being sent + * to the WordPress.com. + * Instead we pass data that will still enable us to do a checksum against the + * Jetpacks data but will prevent us from displaying the data on in the API as well as + * other services. + * + * @since 4.2.0 + * + * @param boolean false prevent post data from bing synced to WordPress.com + * @param mixed $comment WP_COMMENT object + */ + if ( apply_filters( 'jetpack_sync_prevent_sending_comment_data', false, $comment ) ) { + $blocked_comment = new stdClass(); + $blocked_comment->comment_ID = $comment->comment_ID; + $blocked_comment->comment_date = $comment->comment_date; + $blocked_comment->comment_date_gmt = $comment->comment_date_gmt; + $blocked_comment->comment_approved = 'jetpack_sync_blocked'; + return $blocked_comment; + } + + return $comment; + } + + // Comment Meta + function is_whitelisted_comment_meta( $meta_key ) { + return in_array( $meta_key, Jetpack_Sync_Settings::get_setting( 'comment_meta_whitelist' ) ); + } + + function filter_meta( $args ) { + return ( $this->is_whitelisted_comment_meta( $args[2] ) ? $args : false ); + } + + public function expand_comment_ids( $args ) { + list( $comment_ids, $previous_interval_end ) = $args; + $comments = get_comments( + array( + 'include_unapproved' => true, + 'comment__in' => $comment_ids, + 'orderby' => 'comment_ID', + 'order' => 'DESC', + ) + ); + + return array( + $comments, + $this->get_metadata( $comment_ids, 'comment', Jetpack_Sync_Settings::get_setting( 'comment_meta_whitelist' ) ), + $previous_interval_end, + ); + } +} 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..f3af3195 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-constants.php @@ -0,0 +1,125 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php'; + +class Jetpack_Sync_Module_Constants extends Jetpack_Sync_Module { + const CONSTANTS_CHECKSUM_OPTION_NAME = 'jetpack_constants_sync_checksum'; + const CONSTANTS_AWAIT_TRANSIENT_NAME = 'jetpack_sync_constants_await'; + + public function name() { + return 'constants'; + } + + public function init_listeners( $callable ) { + add_action( 'jetpack_sync_constant', $callable, 10, 2 ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_constants', $callable ); + } + + public function init_before_send() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_constants' ) ); + + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_constants', array( $this, 'expand_constants' ) ); + } + + public function reset_data() { + delete_option( self::CONSTANTS_CHECKSUM_OPTION_NAME ); + delete_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ); + } + + function set_constants_whitelist( $constants ) { + $this->constants_whitelist = $constants; + } + + function get_constants_whitelist() { + return Jetpack_Sync_Defaults::get_constants_whitelist(); + } + + function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + /** + * Tells the client to sync all constants to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand constants (should always be true) + */ + do_action( 'jetpack_full_sync_constants', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + function estimate_full_sync_actions( $config ) { + return 1; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_constants' ); + } + + function maybe_sync_constants() { + if ( get_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME ) ) { + return; + } + + set_transient( self::CONSTANTS_AWAIT_TRANSIENT_NAME, microtime( true ), Jetpack_Sync_Defaults::$default_sync_constants_wait_time ); + + $constants = $this->get_all_constants(); + if ( empty( $constants ) ) { + return; + } + + $constants_checksums = (array) get_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, array() ); + + foreach ( $constants as $name => $value ) { + $checksum = $this->get_check_sum( $value ); + // explicitly not using Identical comparison as get_option returns a string + if ( ! $this->still_valid_checksum( $constants_checksums, $name, $checksum ) && ! is_null( $value ) ) { + /** + * Tells the client to sync a constant to the server + * + * @since 4.2.0 + * + * @param string The name of the constant + * @param mixed The value of the constant + */ + do_action( 'jetpack_sync_constant', $name, $value ); + $constants_checksums[ $name ] = $checksum; + } else { + $constants_checksums[ $name ] = $checksum; + } + } + update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums ); + } + + // public so that we don't have to store an option for each constant + function get_all_constants() { + $constants_whitelist = $this->get_constants_whitelist(); + return array_combine( + $constants_whitelist, + array_map( array( $this, 'get_constant' ), $constants_whitelist ) + ); + } + + private function get_constant( $constant ) { + return ( defined( $constant ) ) ? + constant( $constant ) + : null; + } + + public function expand_constants( $args ) { + if ( $args[0] ) { + $constants = $this->get_all_constants(); + $constants_checksums = array(); + foreach ( $constants as $name => $value ) { + $constants_checksums[ $name ] = $this->get_check_sum( $value ); + } + update_option( self::CONSTANTS_CHECKSUM_OPTION_NAME, $constants_checksums ); + return $constants; + } + return $args; + } +} 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..4ad60896 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-full-sync.php @@ -0,0 +1,477 @@ +<?php + +/** + * This class does a full resync of the database by + * enqueuing an outbound action for every single object + * that we care about. + * + * This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained: + * - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database + * - for each object type, we page through the object IDs and enqueue them by firing some monitored actions + * - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls) + * - we fire a trigger for the entire array which the Jetpack_Sync_Listener then serializes and queues. + */ + +class Jetpack_Sync_Module_Full_Sync extends Jetpack_Sync_Module { + const STATUS_OPTION_PREFIX = 'jetpack_sync_full_'; + const FULL_SYNC_TIMEOUT = 3600; + + public function name() { + return 'full-sync'; + } + + function init_full_sync_listeners( $callable ) { + // synthetic actions for full sync + add_action( 'jetpack_full_sync_start', $callable, 10, 2 ); + add_action( 'jetpack_full_sync_end', $callable, 10, 2 ); + add_action( 'jetpack_full_sync_cancelled', $callable ); + } + + function init_before_send() { + // this is triggered after actions have been processed on the server + add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) ); + } + + function start( $module_configs = null ) { + $was_already_running = $this->is_started() && ! $this->is_finished(); + + // remove all evidence of previous full sync items and status + $this->reset_data(); + + if ( $was_already_running ) { + /** + * Fires when a full sync is cancelled. + * + * @since 4.2.0 + */ + do_action( 'jetpack_full_sync_cancelled' ); + } + + $this->update_status_option( 'started', time() ); + $this->update_status_option( 'params', $module_configs ); + + $enqueue_status = array(); + $full_sync_config = array(); + + // default value is full sync + if ( ! is_array( $module_configs ) ) { + $module_configs = array(); + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module_configs[ $module->name() ] = true; + } + } + + // set default configuration, calculate totals, and save configuration if totals > 0 + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module_name = $module->name(); + $module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false; + + if ( ! $module_config ) { + continue; + } + + if ( 'users' === $module_name && 'initial' === $module_config ) { + $module_config = $module->get_initial_sync_user_config(); + } + + $enqueue_status[ $module_name ] = false; + + $total_items = $module->estimate_full_sync_actions( $module_config ); + + // if there's information to process, configure this module + if ( ! is_null( $total_items ) && $total_items > 0 ) { + $full_sync_config[ $module_name ] = $module_config; + $enqueue_status[ $module_name ] = array( + $total_items, // total + 0, // queued + false, // current state + ); + } + } + + $this->set_config( $full_sync_config ); + $this->set_enqueue_status( $enqueue_status ); + + $range = $this->get_content_range( $full_sync_config ); + /** + * 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 + * @since 7.3.0 Added $range arg. + * + * @param $full_sync_config - array + * @param $range array + */ + do_action( 'jetpack_full_sync_start', $full_sync_config, $range ); + + $this->continue_enqueuing( $full_sync_config, $enqueue_status ); + + return true; + } + + function continue_enqueuing( $configs = null, $enqueue_status = null ) { + if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) { + return; + } + + // if full sync queue is full, don't enqueue more items + $max_queue_size_full_sync = Jetpack_Sync_Settings::get_setting( 'max_queue_size_full_sync' ); + $full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' ); + + $available_queue_slots = $max_queue_size_full_sync - $full_sync_queue->size(); + + if ( $available_queue_slots <= 0 ) { + return; + } else { + $remaining_items_to_enqueue = min( Jetpack_Sync_Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots ); + } + + if ( ! $configs ) { + $configs = $this->get_config(); + } + + if ( ! $enqueue_status ) { + $enqueue_status = $this->get_enqueue_status(); + } + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module_name = $module->name(); + + // skip module if not configured for this sync or module is done + if ( ! isset( $configs[ $module_name ] ) + || // no module config + ! $configs[ $module_name ] + || // no enqueue status + ! $enqueue_status[ $module_name ] + || // finished enqueuing this module + true === $enqueue_status[ $module_name ][2] ) { + continue; + } + + list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][2] ); + + $enqueue_status[ $module_name ][2] = $next_enqueue_state; + + // if items were processed, subtract them from the limit + if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) { + $enqueue_status[ $module_name ][1] += $items_enqueued; + $remaining_items_to_enqueue -= $items_enqueued; + } + + // stop processing if we've reached our limit of items to enqueue + if ( 0 >= $remaining_items_to_enqueue ) { + $this->set_enqueue_status( $enqueue_status ); + return; + } + } + + $this->set_enqueue_status( $enqueue_status ); + + // setting autoload to true means that it's faster to check whether we should continue enqueuing + $this->update_status_option( 'queue_finished', time(), true ); + + $range = $this->get_content_range( $configs ); + + /** + * Fires when a full sync ends. This action is serialized + * and sent to the server. + * + * @since 4.2.0 + * @since 7.3.0 Added $range arg. + * + * @param args '' + * @param $range array + */ + do_action( 'jetpack_full_sync_end', '', $range ); + } + + function get_range( $type ) { + global $wpdb; + if ( ! in_array( $type, array( 'comments', 'posts' ) ) ) { + return array(); + } + + switch ( $type ) { + case 'posts': + $table = $wpdb->posts; + $id = 'ID'; + $where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql(); + + break; + case 'comments': + $table = $wpdb->comments; + $id = 'comment_ID'; + $where_sql = Jetpack_Sync_Settings::get_comments_filter_sql(); + break; + } + $results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" ); + if ( isset( $results[0] ) ) { + return $results[0]; + } + + return array(); + } + + private function get_content_range( $config ) { + $range = array(); + // Only when we are sending the whole range do we want to send also the range + if ( isset( $config['posts'] ) && $config['posts'] === true ) { + $range['posts'] = $this->get_range( 'posts' ); + } + + if ( isset( $config['comments'] ) && $config['comments'] === true ) { + $range['comments'] = $this->get_range( 'comments' ); + } + return $range; + } + + function update_sent_progress_action( $actions ) { + // quick way to map to first items with an array of arrays + $actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) ); + + // Total item counts for each action. + $actions_with_total_counts = $this->get_actions_totals( $actions ); + + if ( ! $this->is_started() || $this->is_finished() ) { + return; + } + + if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) { + $this->update_status_option( 'send_started', time() ); + } + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module_actions = $module->get_full_sync_actions(); + $status_option_name = "{$module->name()}_sent"; + $total_option_name = "{$status_option_name}_total"; + $items_sent = $this->get_status_option( $status_option_name, 0 ); + $items_sent_total = $this->get_status_option( $total_option_name, 0 ); + + foreach ( $module_actions as $module_action ) { + if ( isset( $actions_with_counts[ $module_action ] ) ) { + $items_sent += $actions_with_counts[ $module_action ]; + } + + if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) { + $items_sent_total += $actions_with_total_counts[ $module_action ]; + } + } + + if ( $items_sent > 0 ) { + $this->update_status_option( $status_option_name, $items_sent ); + } + + if ( 0 !== $items_sent_total ) { + $this->update_status_option( $total_option_name, $items_sent_total ); + } + } + + if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) { + $this->update_status_option( 'finished', time() ); + } + } + + public function get_action_name( $queue_item ) { + if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) { + return $queue_item[0]; + } + return false; + } + + /** + * Retrieve the total number of items we're syncing in a particular queue item (action). + * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]` + * represents the first (and only) chunk of items to sync in that action. + * + * @param array $queue_item Item of the sync queue that corresponds to a particular action. + * @return int Total number of items in the action. + */ + public function get_action_totals( $queue_item ) { + if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) { + if ( is_array( $queue_item[1][0] ) ) { + // Let's count the items we sync in this action. + return count( $queue_item[1][0] ); + } + // -1 indicates that this action syncs all items by design. + return -1; + } + return 0; + } + + /** + * Retrieve the total number of items for a set of actions, grouped by action name. + * + * @param array $actions An array of actions. + * @return array An array, representing the total number of items, grouped per action. + */ + public function get_actions_totals( $actions ) { + $totals = array(); + + foreach ( $actions as $action ) { + $name = $this->get_action_name( $action ); + $action_totals = $this->get_action_totals( $action ); + if ( ! isset( $totals[ $name ] ) ) { + $totals[ $name ] = 0; + } + $totals[ $name ] += $action_totals; + } + + return $totals; + } + + public function is_started() { + return ! ! $this->get_status_option( 'started' ); + } + + public function is_finished() { + return ! ! $this->get_status_option( 'finished' ); + } + + public function get_status() { + $status = array( + 'started' => $this->get_status_option( 'started' ), + 'queue_finished' => $this->get_status_option( 'queue_finished' ), + 'send_started' => $this->get_status_option( 'send_started' ), + 'finished' => $this->get_status_option( 'finished' ), + 'sent' => array(), + 'sent_total' => array(), + 'queue' => array(), + 'config' => $this->get_status_option( 'params' ), + 'total' => array(), + ); + + $enqueue_status = $this->get_enqueue_status(); + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $name = $module->name(); + + if ( ! isset( $enqueue_status[ $name ] ) ) { + continue; + } + + list( $total, $queued, $state ) = $enqueue_status[ $name ]; + + if ( $total ) { + $status['total'][ $name ] = $total; + } + + if ( $queued ) { + $status['queue'][ $name ] = $queued; + } + + if ( $sent = $this->get_status_option( "{$name}_sent" ) ) { + $status['sent'][ $name ] = $sent; + } + + $sent_total = $this->get_status_option( "{$name}_sent_total" ); + if ( $sent_total ) { + $status['sent_total'][ $name ] = $sent_total; + } + } + + return $status; + } + + public function clear_status() { + $prefix = self::STATUS_OPTION_PREFIX; + Jetpack_Options::delete_raw_option( "{$prefix}_started" ); + Jetpack_Options::delete_raw_option( "{$prefix}_params" ); + Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" ); + Jetpack_Options::delete_raw_option( "{$prefix}_send_started" ); + Jetpack_Options::delete_raw_option( "{$prefix}_finished" ); + + $this->delete_enqueue_status(); + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" ); + Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" ); + } + } + + public function reset_data() { + $this->clear_status(); + $this->delete_config(); + require_once dirname( __FILE__ ) . '/class.jetpack-sync-listener.php'; + $listener = Jetpack_Sync_Listener::get_instance(); + $listener->get_full_sync_queue()->reset(); + } + + private function get_status_option( $name, $default = null ) { + $value = Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default ); + + return is_numeric( $value ) ? intval( $value ) : $value; + } + + private function update_status_option( $name, $value, $autoload = false ) { + Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload ); + } + + private function set_enqueue_status( $new_status ) { + Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status ); + } + + private function delete_enqueue_status() { + return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' ); + } + + private function get_enqueue_status() { + return Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' ); + } + + private function set_config( $config ) { + Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config ); + } + + private function delete_config() { + return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' ); + } + + private function get_config() { + return Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' ); + } + + private function write_option( $name, $value ) { + // we write our own option updating code to bypass filters/caching/etc on set_option/get_option + global $wpdb; + $serialized_value = maybe_serialize( $value ); + // try updating, if no update then insert + // TODO: try to deal with the fact that unchanged values can return updated_num = 0 + // below we used "insert ignore" to at least suppress the resulting error + $updated_num = $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", + $serialized_value, + $name + ) + ); + + if ( ! $updated_num ) { + $updated_num = $wpdb->query( + $wpdb->prepare( + "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )", + $name, + $serialized_value + ) + ); + } + return $updated_num; + } + + private function read_option( $name, $default = null ) { + global $wpdb; + $value = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", + $name + ) + ); + $value = maybe_unserialize( $value ); + + if ( $value === null && $default !== null ) { + return $default; + } + + return $value; + } +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-import.php b/plugins/jetpack/sync/class.jetpack-sync-module-import.php new file mode 100644 index 00000000..4b5dd83d --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-import.php @@ -0,0 +1,170 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + +class Jetpack_Sync_Module_Import extends Jetpack_Sync_Module { + + /** + * Tracks which actions have already been synced for the import + * to prevent the same event from being triggered a second time + * + * @var array + */ + private $synced_actions = array(); + + /** + * A mapping of action types to sync action name. + * Keys are the name of the import action. + * Values are the resulting sync action. + * + * Note: import_done and import_end both intentionally map to + * jetpack_sync_import_end, as they both track the same type of action, + * the successful completion of an import. Different import plugins use + * differently named actions, and this is an attempt to consolidate. + * + * @var array + */ + private static $import_sync_action_map = array( + 'import_start' => 'jetpack_sync_import_start', + 'import_done' => 'jetpack_sync_import_end', + 'import_end' => 'jetpack_sync_import_end', + ); + + public function name() { + return 'import'; + } + + public function init_listeners( $callable ) { + add_action( 'export_wp', $callable ); + add_action( 'jetpack_sync_import_start', $callable, 10, 2 ); + add_action( 'jetpack_sync_import_end', $callable, 10, 2 ); + + // WordPress. + add_action( 'import_start', array( $this, 'sync_import_action' ) ); + + // Movable type, RSS, Livejournal. + add_action( 'import_done', array( $this, 'sync_import_action' ) ); + + // WordPress, Blogger, Livejournal, woo tax rate. + add_action( 'import_end', array( $this, 'sync_import_action' ) ); + } + + public function set_defaults() { + $this->synced_actions = array(); + } + + public function sync_import_action( $importer ) { + $import_action = current_filter(); + // Map action to event name. + $sync_action = self::$import_sync_action_map[ $import_action ]; + + // Only sync each action once per import. + if ( array_key_exists( $sync_action, $this->synced_actions ) && $this->synced_actions[ $sync_action ] ) { + return; + } + + // Mark this action as synced. + $this->synced_actions[ $sync_action ] = true; + + // Prefer self-reported $importer value. + if ( ! $importer ) { + // Fall back to inferring by calling class name. + $importer = self::get_calling_importer_class(); + } + + // Get $importer from known_importers. + $known_importers = Jetpack_Sync_Settings::get_setting( 'known_importers' ); + if ( isset( $known_importers[ $importer ] ) ) { + $importer = $known_importers[ $importer ]; + } + + $importer_name = $this->get_importer_name( $importer ); + + switch ( $sync_action ) { + case 'jetpack_sync_import_start': + /** + * Used for syncing the start of an import + * + * @since 7.3.0 + * + * @module sync + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + * @param string $importer_name The name reported by the importer, or 'Unknown Importer'. + */ + do_action( 'jetpack_sync_import_start', $importer, $importer_name ); + break; + + case 'jetpack_sync_import_end': + /** + * Used for syncing the end of an import + * + * @since 7.3.0 + * + * @module sync + * + * @param string $importer Either a string reported by the importer, the class name of the importer, or 'unknown'. + * @param string $importer_name The name reported by the importer, or 'Unknown Importer'. + */ + do_action( 'jetpack_sync_import_end', $importer, $importer_name ); + break; + } + } + + private function get_importer_name( $importer ) { + $importers = get_importers(); + return isset( $importers[ $importer ] ) ? $importers[ $importer ][0] : 'Unknown Importer'; + } + + /** + * Determine the class that extends `WP_Importer` which is responsible for + * the current action. Designed to be used within an action handler. + * + * @return string The name of the calling class, or 'unknown'. + */ + private static function get_calling_importer_class() { + // If WP_Importer doesn't exist, neither will any importer that extends it. + if ( ! class_exists( 'WP_Importer', false ) ) { + return 'unknown'; + } + + $action = current_filter(); + $backtrace = debug_backtrace( false ); //phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.debug_backtrace_optionsFound,WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace + + $do_action_pos = -1; + $backtrace_len = count( $backtrace ); + for ( $i = 0; $i < $backtrace_len; $i++ ) { + // Find the location in the stack of the calling action. + if ( 'do_action' === $backtrace[ $i ]['function'] && $action === $backtrace[ $i ]['args'][0] ) { + $do_action_pos = $i; + break; + } + } + + // If the action wasn't called, the calling class is unknown. + if ( -1 === $do_action_pos ) { + return 'unknown'; + } + + // Continue iterating the stack looking for a caller that extends WP_Importer. + for ( $i = $do_action_pos + 1; $i < $backtrace_len; $i++ ) { + // If there is no class on the trace, continue. + if ( ! isset( $backtrace[ $i ]['class'] ) ) { + continue; + } + + $class_name = $backtrace[ $i ]['class']; + + // Check if the class extends WP_Importer. + if ( class_exists( $class_name, false ) ) { + $parents = class_parents( $class_name, false ); + if ( $parents && in_array( 'WP_Importer', $parents, true ) ) { + return $class_name; + } + } + } + + // If we've exhausted the stack without a match, the calling class is unknown. + return 'unknown'; + } +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-menus.php b/plugins/jetpack/sync/class.jetpack-sync-module-menus.php new file mode 100644 index 00000000..86e2279c --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-menus.php @@ -0,0 +1,78 @@ +<?php + +class Jetpack_Sync_Module_Menus extends Jetpack_Sync_Module { + private $nav_items_just_added = array(); + + function name() { + return 'menus'; + } + + public function init_listeners( $callable ) { + add_action( 'wp_create_nav_menu', $callable, 10, 2 ); + add_action( 'wp_update_nav_menu', array( $this, 'update_nav_menu' ), 10, 2 ); + add_action( 'wp_add_nav_menu_item', array( $this, 'update_nav_menu_add_item' ), 10, 3 ); + add_action( 'wp_update_nav_menu_item', array( $this, 'update_nav_menu_update_item' ), 10, 3 ); + add_action( 'post_updated', array( $this, 'remove_just_added_menu_item' ), 10, 2 ); + + add_action( 'jetpack_sync_updated_nav_menu', $callable, 10, 2 ); + add_action( 'jetpack_sync_updated_nav_menu_add_item', $callable, 10, 4 ); + add_action( 'jetpack_sync_updated_nav_menu_update_item', $callable, 10, 4 ); + add_action( 'delete_nav_menu', $callable, 10, 3 ); + } + + public function update_nav_menu( $menu_id, $menu_data = array() ) { + if ( empty( $menu_data ) ) { + return; + } + /** + * Helps sync log that a nav menu was updated. + * + * @since 5.0.0 + * + * @param int $menu_id, the id of the menu + * @param object $menu_data + */ + do_action( 'jetpack_sync_updated_nav_menu', $menu_id, $menu_data ); + } + + public function update_nav_menu_add_item( $menu_id, $nav_item_id, $nav_item_args ) { + $menu_data = wp_get_nav_menu_object( $menu_id ); + $this->nav_items_just_added[] = $nav_item_id; + /** + * Helps sync log that a new menu item was added. + * + * @since 5.0.0 + * + * @param int $menu_id, the id of the menu + * @param object $menu_data + * @param int $nav_item_id + * @param int $nav_item_args + */ + do_action( 'jetpack_sync_updated_nav_menu_add_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args ); + } + + public function update_nav_menu_update_item( $menu_id, $nav_item_id, $nav_item_args ) { + if ( in_array( $nav_item_id, $this->nav_items_just_added ) ) { + return; + } + $menu_data = wp_get_nav_menu_object( $menu_id ); + /** + * Helps sync log that an update to the menu item happened. + * + * @since 5.0.0 + * + * @param int $menu_id, the id of the menu + * @param object $menu_data + * @param int $nav_item_id + * @param int $nav_item_args + */ + do_action( 'jetpack_sync_updated_nav_menu_update_item', $menu_id, $menu_data, $nav_item_id, $nav_item_args ); + } + + public function remove_just_added_menu_item( $nav_item_id, $post_after ) { + if ( 'nav_menu_item' !== $post_after->post_type ) { + return; + } + $this->nav_items_just_added = array_diff( $this->nav_items_just_added, array( $nav_item_id ) ); + } +} 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..e3d43544 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-meta.php @@ -0,0 +1,62 @@ +<?php + +class Jetpack_Sync_Module_Meta extends Jetpack_Sync_Module { + + public function name() { + return 'meta'; + } + + /** + * This implementation of get_objects_by_id() is a bit hacky since we're not passing in an array of meta IDs, + * but instead an array of post or comment IDs for which to retrieve meta for. On top of that, + * we also pass in an associative array where we expect there to be 'meta_key' and 'ids' keys present. + * + * This seemed to be required since if we have missing meta on WP.com and need to fetch it, we don't know what + * the meta key is, but we do know that we have missing meta for a given post or comment. + * + * @param string $object_type The type of object for which we retrieve meta. Either 'post' or 'comment' + * @param array $config Must include 'meta_key' and 'ids' keys + * + * @return array + */ + public function get_objects_by_id( $object_type, $config ) { + global $wpdb; + + $table = _get_meta_table( $object_type ); + + if ( ! $table ) { + return array(); + } + + if ( ! isset( $config['meta_key'] ) || ! isset( $config['ids'] ) || ! is_array( $config['ids'] ) ) { + return array(); + } + + $meta_key = $config['meta_key']; + $ids = $config['ids']; + $object_id_column = $object_type . '_id'; + + // Sanitize so that the array only has integer values + $ids_string = implode( ', ', array_map( 'intval', $ids ) ); + $metas = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$table} WHERE {$object_id_column} IN ( {$ids_string} ) AND meta_key = %s", + $meta_key + ) + ); + + $meta_objects = array(); + foreach ( (array) $metas as $meta_object ) { + $meta_object = (array) $meta_object; + $meta_objects[ $meta_object[ $object_id_column ] ] = array( + 'meta_type' => $object_type, + 'meta_id' => $meta_object['meta_id'], + 'meta_key' => $meta_key, + 'meta_value' => $meta_object['meta_value'], + 'object_id' => $meta_object[ $object_id_column ], + ); + } + + return $meta_objects; + } +} 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..011e9bba --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-network-options.php @@ -0,0 +1,116 @@ +<?php + +class Jetpack_Sync_Module_Network_Options extends Jetpack_Sync_Module { + private $network_options_whitelist; + + public function name() { + return 'network_options'; + } + + public function init_listeners( $callable ) { + if ( ! is_multisite() ) { + return; + } + + // multi site network options + add_action( 'add_site_option', $callable, 10, 2 ); + add_action( 'update_site_option', $callable, 10, 3 ); + add_action( 'delete_site_option', $callable, 10, 1 ); + + $whitelist_network_option_handler = array( $this, 'whitelist_network_options' ); + add_filter( 'jetpack_sync_before_enqueue_delete_site_option', $whitelist_network_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_add_site_option', $whitelist_network_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_update_site_option', $whitelist_network_option_handler ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_network_options', $callable ); + } + + public function init_before_send() { + if ( ! is_multisite() ) { + return; + } + + // full sync + add_filter( + 'jetpack_sync_before_send_jetpack_full_sync_network_options', + array( + $this, + 'expand_network_options', + ) + ); + } + + public function set_defaults() { + $this->network_options_whitelist = Jetpack_Sync_Defaults::$default_network_options_whitelist; + } + + function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + if ( ! is_multisite() ) { + return array( 0, true ); + } + + /** + * Tells the client to sync all options to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand options (should always be true) + */ + do_action( 'jetpack_full_sync_network_options', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + function estimate_full_sync_actions( $config ) { + if ( ! is_multisite() ) { + return 0; + } + + return 1; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_network_options' ); + } + + function get_all_network_options() { + $options = array(); + foreach ( $this->network_options_whitelist as $option ) { + $options[ $option ] = get_site_option( $option ); + } + + return $options; + } + + function set_network_options_whitelist( $options ) { + $this->network_options_whitelist = $options; + } + + function get_network_options_whitelist() { + return $this->network_options_whitelist; + } + + // reject non-whitelisted network options + function whitelist_network_options( $args ) { + if ( ! $this->is_whitelisted_network_option( $args[0] ) ) { + return false; + } + + return $args; + } + + function is_whitelisted_network_option( $option ) { + return is_multisite() && in_array( $option, $this->network_options_whitelist ); + } + + public function expand_network_options( $args ) { + if ( $args[0] ) { + return $this->get_all_network_options(); + } + + return $args; + } +} 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..287e73b9 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-options.php @@ -0,0 +1,173 @@ +<?php + +class Jetpack_Sync_Module_Options extends Jetpack_Sync_Module { + private $options_whitelist, $options_contentless; + + public function name() { + return 'options'; + } + + public function init_listeners( $callable ) { + // options + add_action( 'added_option', $callable, 10, 2 ); + add_action( 'updated_option', $callable, 10, 3 ); + add_action( 'deleted_option', $callable, 10, 1 ); + + // Sync Core Icon: Detect changes in Core's Site Icon and make it syncable. + add_action( 'add_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + add_action( 'update_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + add_action( 'delete_option_site_icon', array( $this, 'jetpack_sync_core_icon' ) ); + + $whitelist_option_handler = array( $this, 'whitelist_options' ); + add_filter( 'jetpack_sync_before_enqueue_deleted_option', $whitelist_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_added_option', $whitelist_option_handler ); + add_filter( 'jetpack_sync_before_enqueue_updated_option', $whitelist_option_handler ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_options', $callable ); + } + + public function init_before_send() { + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_options', array( $this, 'expand_options' ) ); + } + + public function set_defaults() { + $this->update_options_whitelist(); + $this->update_options_contentless(); + } + + public function set_late_default() { + + /** This filter is already documented in json-endpoints/jetpack/class.wpcom-json-api-get-option-endpoint.php */ + $late_options = apply_filters( 'jetpack_options_whitelist', array() ); + if ( ! empty( $late_options ) && is_array( $late_options ) ) { + $this->options_whitelist = array_merge( $this->options_whitelist, $late_options ); + } + } + + function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + /** + * Tells the client to sync all options to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand options (should always be true) + */ + do_action( 'jetpack_full_sync_options', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + public function estimate_full_sync_actions( $config ) { + return 1; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_options' ); + } + + // Is public so that we don't have to store so much data all the options twice. + function get_all_options() { + $options = array(); + $random_string = wp_generate_password(); + foreach ( $this->options_whitelist as $option ) { + $option_value = get_option( $option, $random_string ); + if ( $option_value !== $random_string ) { + $options[ $option ] = $option_value; + } + } + + // add theme mods + $theme_mods_option = 'theme_mods_' . get_option( 'stylesheet' ); + $theme_mods_value = get_option( $theme_mods_option, $random_string ); + if ( $theme_mods_value === $random_string ) { + return $options; + } + $this->filter_theme_mods( $theme_mods_value ); + $options[ $theme_mods_option ] = $theme_mods_value; + return $options; + } + + function update_options_whitelist() { + $this->options_whitelist = Jetpack_Sync_Defaults::get_options_whitelist(); + } + + function set_options_whitelist( $options ) { + $this->options_whitelist = $options; + } + + function get_options_whitelist() { + return $this->options_whitelist; + } + + function update_options_contentless() { + $this->options_contentless = Jetpack_Sync_Defaults::get_options_contentless(); + } + + function get_options_contentless() { + return $this->options_contentless; + } + + function whitelist_options( $args ) { + // Reject non-whitelisted options + if ( ! $this->is_whitelisted_option( $args[0] ) ) { + return false; + } + + // filter our weird array( false ) value for theme_mods_* + if ( 'theme_mods_' === substr( $args[0], 0, 11 ) ) { + $this->filter_theme_mods( $args[1] ); + if ( isset( $args[2] ) ) { + $this->filter_theme_mods( $args[2] ); + } + } + + // Set value(s) of contentless option to empty string(s) + if ( $this->is_contentless_option( $args[0] ) ) { + // Create a new array matching length of $args, containing empty strings + $empty = array_fill( 0, count( $args ), '' ); + $empty[0] = $args[0]; + return $empty; + } + + return $args; + } + + function is_whitelisted_option( $option ) { + return in_array( $option, $this->options_whitelist ) || 'theme_mods_' === substr( $option, 0, 11 ); + } + + private function is_contentless_option( $option ) { + return in_array( $option, $this->options_contentless ); + } + + private function filter_theme_mods( &$value ) { + if ( is_array( $value ) && isset( $value[0] ) ) { + unset( $value[0] ); + } + } + + function jetpack_sync_core_icon() { + $url = get_site_icon_url(); + + 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 ); + } elseif ( 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..876526ab --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-plugins.php @@ -0,0 +1,289 @@ +<?php + +class Jetpack_Sync_Module_Plugins extends Jetpack_Sync_Module { + + private $action_handler; + private $plugin_info = array(); + private $plugins = array(); + + public function name() { + return 'plugins'; + } + + public function init_listeners( $callable ) { + $this->action_handler = $callable; + + add_action( 'deleted_plugin', array( $this, 'deleted_plugin' ), 10, 2 ); + add_action( 'activated_plugin', $callable, 10, 2 ); + add_action( 'deactivated_plugin', $callable, 10, 2 ); + add_action( 'delete_plugin', array( $this, 'delete_plugin' ) ); + add_filter( 'upgrader_pre_install', array( $this, 'populate_plugins' ), 10, 1 ); + add_action( 'upgrader_process_complete', array( $this, 'on_upgrader_completion' ), 10, 2 ); + add_action( 'jetpack_plugin_installed', $callable, 10, 1 ); + add_action( 'jetpack_plugin_update_failed', $callable, 10, 4 ); + add_action( 'jetpack_plugins_updated', $callable, 10, 2 ); + add_action( 'admin_action_update', array( $this, 'check_plugin_edit' ) ); + add_action( 'jetpack_edited_plugin', $callable, 10, 2 ); + add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'plugin_edit_ajax' ), 0 ); + } + + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_activated_plugin', array( $this, 'expand_plugin_data' ) ); + add_filter( 'jetpack_sync_before_send_deactivated_plugin', array( $this, 'expand_plugin_data' ) ); + // Note that we don't simply 'expand_plugin_data' on the 'delete_plugin' action here because the plugin file is deleted when that action finishes + } + public function populate_plugins( $response ) { + $this->plugins = get_plugins(); + return $response; + } + public function on_upgrader_completion( $upgrader, $details ) { + if ( ! isset( $details['type'] ) ) { + return; + } + if ( 'plugin' != $details['type'] ) { + return; + } + + if ( ! isset( $details['action'] ) ) { + return; + } + + $plugins = ( isset( $details['plugins'] ) ? $details['plugins'] : null ); + if ( empty( $plugins ) ) { + $plugins = ( isset( $details['plugin'] ) ? array( $details['plugin'] ) : null ); + } + + // for plugin installer + if ( empty( $plugins ) && method_exists( $upgrader, 'plugin_info' ) ) { + $plugins = array( $upgrader->plugin_info() ); + } + + if ( empty( $plugins ) ) { + return; // We shouldn't be here + } + + switch ( $details['action'] ) { + case 'update': + $state = array( + 'is_autoupdate' => Jetpack_Constants::is_true( 'JETPACK_PLUGIN_AUTOUPDATE' ), + ); + $errors = $this->get_errors( $upgrader->skin ); + if ( $errors ) { + foreach ( $plugins as $slug ) { + /** + * Sync that a plugin update failed + * + * @since 5.8.0 + * + * @module sync + * + * @param string $plugin , Plugin slug + * @param string Error code + * @param string Error message + */ + do_action( 'jetpack_plugin_update_failed', $this->get_plugin_info( $slug ), $errors['code'], $errors['message'], $state ); + } + + return; + } + /** + * Sync that a plugin update + * + * @since 5.8.0 + * + * @module sync + * + * @param array () $plugin, Plugin Data + */ + do_action( 'jetpack_plugins_updated', array_map( array( $this, 'get_plugin_info' ), $plugins ), $state ); + break; + case 'install': + } + + if ( 'install' === $details['action'] ) { + /** + * Signals to the sync listener that a plugin was installed and a sync action + * reflecting the installation and the plugin info should be sent + * + * @since 5.8.0 + * + * @module sync + * + * @param array () $plugin, Plugin Data + */ + do_action( 'jetpack_plugin_installed', array_map( array( $this, 'get_plugin_info' ), $plugins ) ); + + return; + } + } + + private function get_plugin_info( $slug ) { + $plugins = get_plugins(); // Get the most up to date info + if ( isset( $plugins[ $slug ] ) ) { + return array_merge( array( 'slug' => $slug ), $plugins[ $slug ] ); + }; + // Try grabbing the info from before the update + return isset( $this->plugins[ $slug ] ) ? array_merge( array( 'slug' => $slug ), $this->plugins[ $slug ] ) : array( 'slug' => $slug ); + } + + private function get_errors( $skin ) { + $errors = method_exists( $skin, 'get_errors' ) ? $skin->get_errors() : null; + if ( is_wp_error( $errors ) ) { + $error_code = $errors->get_error_code(); + if ( ! empty( $error_code ) ) { + return array( + 'code' => $error_code, + 'message' => $errors->get_error_message(), + ); + } + } + + if ( isset( $skin->result ) ) { + $errors = $skin->result; + if ( is_wp_error( $errors ) ) { + return array( + 'code' => $errors->get_error_code(), + 'message' => $errors->get_error_message(), + ); + } + + if ( false == $skin->result ) { + return array( + 'code' => 'unknown', + 'message' => __( 'Unknown Plugin Update Failure', 'jetpack' ), + ); + } + } + return false; + } + + public function check_plugin_edit() { + $screen = get_current_screen(); + if ( 'plugin-editor' !== $screen->base || + ! isset( $_POST['newcontent'] ) || + ! isset( $_POST['plugin'] ) + ) { + return; + } + + $plugin = $_POST['plugin']; + $plugins = get_plugins(); + if ( ! isset( $plugins[ $plugin ] ) ) { + return; + } + + /** + * Helps Sync log that a plugin was edited + * + * @since 4.9.0 + * + * @param string $plugin, Plugin slug + * @param mixed $plugins[ $plugin ], Array of plugin data + */ + do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] ); + } + + public function plugin_edit_ajax() { + // this validation is based on wp_edit_theme_plugin_file() + $args = wp_unslash( $_POST ); + if ( empty( $args['file'] ) ) { + return; + } + + $file = $args['file']; + if ( 0 !== validate_file( $file ) ) { + return; + } + + if ( ! isset( $args['newcontent'] ) ) { + return; + } + + if ( ! isset( $args['nonce'] ) ) { + return; + } + + if ( empty( $args['plugin'] ) ) { + return; + } + + $plugin = $args['plugin']; + if ( ! current_user_can( 'edit_plugins' ) ) { + return; + } + + if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) { + return; + } + $plugins = get_plugins(); + if ( ! array_key_exists( $plugin, $plugins ) ) { + return; + } + + if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) { + return; + } + + $real_file = WP_PLUGIN_DIR . '/' . $file; + + if ( ! is_writeable( $real_file ) ) { + return; + } + + $file_pointer = fopen( $real_file, 'w+' ); + if ( false === $file_pointer ) { + return; + } + fclose( $file_pointer ); + /** + * This action is documented already in this file + */ + do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] ); + } + + public function delete_plugin( $plugin_path ) { + $full_plugin_path = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_path; + + // Checking for file existence because some sync plugin module tests simulate plugin installation and deletion without putting file on disk + if ( file_exists( $full_plugin_path ) ) { + $all_plugin_data = get_plugin_data( $full_plugin_path ); + $data = array( + 'name' => $all_plugin_data['Name'], + 'version' => $all_plugin_data['Version'], + ); + } else { + $data = array( + 'name' => $plugin_path, + 'version' => 'unknown', + ); + } + + $this->plugin_info[ $plugin_path ] = $data; + } + + public function deleted_plugin( $plugin_path, $is_deleted ) { + call_user_func( $this->action_handler, $plugin_path, $is_deleted, $this->plugin_info[ $plugin_path ] ); + unset( $this->plugin_info[ $plugin_path ] ); + } + + public function expand_plugin_data( $args ) { + $plugin_path = $args[0]; + $plugin_data = array(); + + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + $all_plugins = get_plugins(); + if ( isset( $all_plugins[ $plugin_path ] ) ) { + $all_plugin_data = $all_plugins[ $plugin_path ]; + $plugin_data['name'] = $all_plugin_data['Name']; + $plugin_data['version'] = $all_plugin_data['Version']; + } + + return array( + $args[0], + $args[1], + $plugin_data, + ); + } +} 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..0384b64f --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-posts.php @@ -0,0 +1,431 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + +class Jetpack_Sync_Module_Posts extends Jetpack_Sync_Module { + + private $just_published = array(); + private $previous_status = array(); + private $action_handler; + private $import_end = false; + + const DEFAULT_PREVIOUS_STATE = 'new'; + + public function name() { + return 'posts'; + } + + public function get_object_by_id( $object_type, $id ) { + if ( $object_type === 'post' && $post = get_post( intval( $id ) ) ) { + return $this->filter_post_content_and_add_links( $post ); + } + + return false; + } + + public function init_listeners( $callable ) { + $this->action_handler = $callable; + + add_action( 'wp_insert_post', array( $this, 'wp_insert_post' ), 11, 3 ); + add_action( 'jetpack_sync_save_post', $callable, 10, 4 ); + + add_action( 'deleted_post', $callable, 10 ); + add_action( 'jetpack_published_post', $callable, 10, 2 ); + + add_action( 'transition_post_status', array( $this, 'save_published' ), 10, 3 ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_post', array( $this, 'filter_blacklisted_post_types' ) ); + + // listen for meta changes + $this->init_listeners_for_meta_type( 'post', $callable ); + $this->init_meta_whitelist_handler( 'post', array( $this, 'filter_meta' ) ); + + add_action( 'jetpack_daily_akismet_meta_cleanup_before', array( $this, 'daily_akismet_meta_cleanup_before' ) ); + add_action( 'jetpack_daily_akismet_meta_cleanup_after', array( $this, 'daily_akismet_meta_cleanup_after' ) ); + add_action( 'jetpack_post_meta_batch_delete', $callable, 10, 2 ); + + } + + + public function daily_akismet_meta_cleanup_before( $feedback_ids ) { + remove_action( 'deleted_post_meta', $this->action_handler ); + /** + * Used for syncing deletion of batch post meta + * + * @since 6.1.0 + * + * @module sync + * + * @param array $feedback_ids feedback post IDs + * @param string $meta_key to be deleted + */ + do_action( 'jetpack_post_meta_batch_delete', $feedback_ids, '_feedback_akismet_values' ); + } + + public function daily_akismet_meta_cleanup_after( $feedback_ids ) { + add_action( 'deleted_post_meta', $this->action_handler ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_posts', $callable ); // also sends post meta + } + + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_sync_save_post', array( $this, 'expand_jetpack_sync_save_post' ) ); + + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->posts WHERE " . $this->get_where_sql( $config ); + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + private function get_where_sql( $config ) { + $where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql(); + + // config is a list of post IDs to sync + if ( is_array( $config ) ) { + $where_sql .= ' AND ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return $where_sql; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_posts' ); + } + + /** + * Process content before send + * + * @param array $args wp_insert_post arguments + * + * @return array + */ + function expand_jetpack_sync_save_post( $args ) { + list( $post_id, $post, $update, $previous_state ) = $args; + return array( $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state ); + } + + function filter_blacklisted_post_types( $args ) { + $post = $args[1]; + + if ( in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ) ) { + return false; + } + + return $args; + } + + // Meta + function filter_meta( $args ) { + if ( $this->is_post_type_allowed( $args[1] ) && $this->is_whitelisted_post_meta( $args[2] ) ) { + return $args; + } + + return false; + } + + function is_whitelisted_post_meta( $meta_key ) { + // _wpas_skip_ is used by publicize + return in_array( $meta_key, Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ) || wp_startswith( $meta_key, '_wpas_skip_' ); + } + + function is_post_type_allowed( $post_id ) { + $post = get_post( intval( $post_id ) ); + if ( $post->post_type ) { + return ! in_array( $post->post_type, Jetpack_Sync_Settings::get_setting( 'post_types_blacklist' ) ); + } + return false; + } + + function remove_embed() { + global $wp_embed; + remove_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 ); + // remove the embed shortcode since we would do the part later. + remove_shortcode( 'embed' ); + // Attempts to embed all URLs in a post + remove_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 ); + } + + function add_embed() { + global $wp_embed; + add_filter( 'the_content', array( $wp_embed, 'run_shortcode' ), 8 ); + // Shortcode placeholder for strip_shortcodes() + add_shortcode( 'embed', '__return_false' ); + // Attempts to embed all URLs in a post + add_filter( 'the_content', array( $wp_embed, 'autoembed' ), 8 ); + } + + // Expands wp_insert_post to include filtered content + function filter_post_content_and_add_links( $post_object ) { + global $post; + $post = $post_object; + + // return non existant post + $post_type = get_post_type_object( $post->post_type ); + if ( empty( $post_type ) || ! is_object( $post_type ) ) { + $non_existant_post = new stdClass(); + $non_existant_post->ID = $post->ID; + $non_existant_post->post_modified = $post->post_modified; + $non_existant_post->post_modified_gmt = $post->post_modified_gmt; + $non_existant_post->post_status = 'jetpack_sync_non_registered_post_type'; + $non_existant_post->post_type = $post->post_type; + + return $non_existant_post; + } + /** + * Filters whether to prevent sending post data to .com + * + * Passing true to the filter will prevent the post data from being sent + * to the WordPress.com. + * Instead we pass data that will still enable us to do a checksum against the + * Jetpacks data but will prevent us from displaying the data on in the API as well as + * other services. + * + * @since 4.2.0 + * + * @param boolean false prevent post data from being synced to WordPress.com + * @param mixed $post WP_POST object + */ + if ( apply_filters( 'jetpack_sync_prevent_sending_post_data', false, $post ) ) { + // We only send the bare necessary object to be able to create a checksum. + $blocked_post = new stdClass(); + $blocked_post->ID = $post->ID; + $blocked_post->post_modified = $post->post_modified; + $blocked_post->post_modified_gmt = $post->post_modified_gmt; + $blocked_post->post_status = 'jetpack_sync_blocked'; + $blocked_post->post_type = $post->post_type; + + return $blocked_post; + } + + // lets not do oembed just yet. + $this->remove_embed(); + + if ( 0 < strlen( $post->post_password ) ) { + $post->post_password = 'auto-' . wp_generate_password( 10, false ); + } + + /** This filter is already documented in core. wp-includes/post-template.php */ + if ( Jetpack_Sync_Settings::get_setting( 'render_filtered_content' ) && $post_type->public ) { + global $shortcode_tags; + /** + * Filter prevents some shortcodes from expanding. + * + * Since we can can expand some type of shortcode better on the .com side and make the + * expansion more relevant to contexts. For example [galleries] and subscription emails + * + * @since 4.5.0 + * + * @param array of shortcode tags to remove. + */ + $shortcodes_to_remove = apply_filters( + 'jetpack_sync_do_not_expand_shortcodes', + array( + 'gallery', + 'slideshow', + ) + ); + $removed_shortcode_callbacks = array(); + foreach ( $shortcodes_to_remove as $shortcode ) { + if ( isset( $shortcode_tags[ $shortcode ] ) ) { + $removed_shortcode_callbacks[ $shortcode ] = $shortcode_tags[ $shortcode ]; + } + } + + array_map( 'remove_shortcode', array_keys( $removed_shortcode_callbacks ) ); + + $post->post_content_filtered = apply_filters( 'the_content', $post->post_content ); + $post->post_excerpt_filtered = apply_filters( 'the_excerpt', $post->post_excerpt ); + + foreach ( $removed_shortcode_callbacks as $shortcode => $callback ) { + add_shortcode( $shortcode, $callback ); + } + } + + $this->add_embed(); + + if ( has_post_thumbnail( $post->ID ) ) { + $image_attributes = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' ); + if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) { + $post->featured_image = $image_attributes[0]; + } + } + + $post->permalink = get_permalink( $post->ID ); + $post->shortlink = wp_get_shortlink( $post->ID ); + + if ( function_exists( 'amp_get_permalink' ) ) { + $post->amp_permalink = amp_get_permalink( $post->ID ); + } + + return $post; + } + + public function save_published( $new_status, $old_status, $post ) { + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + $this->just_published[ $post->ID ] = true; + } + + $this->previous_status[ $post->ID ] = $old_status; + } + + /* + * When publishing or updating a post, the Gutenberg editor sends two requests: + * 1. sent to WP REST API endpoint `wp-json/wp/v2/posts/$id` + * 2. sent to wp-admin/post.php `?post=$id&action=edit&classic-editor=1&meta_box=1` + * + * The 2nd request is to update post meta, which is not supported on WP REST API. + * When syncing post data, we will include if this was a meta box update. + */ + public function is_gutenberg_meta_box_update() { + return ( + isset( $_POST['action'], $_GET['classic-editor'], $_GET['meta_box'] ) && + 'editpost' === $_POST['action'] && + '1' === $_GET['classic-editor'] && + '1' === $_GET['meta_box'] + ); + } + + public function wp_insert_post( $post_ID, $post = null, $update = null ) { + if ( ! is_numeric( $post_ID ) || is_null( $post ) ) { + return; + } + + // workaround for https://github.com/woocommerce/woocommerce/issues/18007 + if ( $post && 'shop_order' === $post->post_type ) { + $post = get_post( $post_ID ); + } + + $previous_status = isset( $this->previous_status[ $post_ID ] ) ? + $this->previous_status[ $post_ID ] : + self::DEFAULT_PREVIOUS_STATE; + + $just_published = isset( $this->just_published[ $post_ID ] ) ? + $this->just_published[ $post_ID ] : + false; + + $state = array( + 'is_auto_save' => (bool) Jetpack_Constants::get_constant( 'DOING_AUTOSAVE' ), + 'previous_status' => $previous_status, + 'just_published' => $just_published, + 'is_gutenberg_meta_box_update' => $this->is_gutenberg_meta_box_update(), + ); + /** + * Filter that is used to add to the post flags ( meta data ) when a post gets published + * + * @since 5.8.0 + * + * @param int $post_ID the post ID + * @param mixed $post WP_POST object + * @param bool $update Whether this is an existing post being updated or not. + * @param mixed $state state + * + * @module sync + */ + do_action( 'jetpack_sync_save_post', $post_ID, $post, $update, $state ); + unset( $this->previous_status[ $post_ID ] ); + $this->send_published( $post_ID, $post ); + } + + public function send_published( $post_ID, $post ) { + if ( ! isset( $this->just_published[ $post_ID ] ) ) { + return; + } + + // Post revisions cause race conditions where this send_published add the action before the actual post gets synced + if ( wp_is_post_autosave( $post ) || wp_is_post_revision( $post ) ) { + return; + } + + $post_flags = array( + 'post_type' => $post->post_type, + ); + + $author_user_object = get_user_by( 'id', $post->post_author ); + if ( $author_user_object ) { + $post_flags['author'] = array( + 'id' => $post->post_author, + 'wpcom_user_id' => get_user_meta( $post->post_author, 'wpcom_user_id', true ), + 'display_name' => $author_user_object->display_name, + 'email' => $author_user_object->user_email, + 'translated_role' => Jetpack::translate_user_to_role( $author_user_object ), + ); + } + + /** + * Filter that is used to add to the post flags ( meta data ) when a post gets published + * + * @since 4.4.0 + * + * @param mixed array post flags that are added to the post + * @param mixed $post WP_POST object + */ + $flags = apply_filters( 'jetpack_published_post_flags', $post_flags, $post ); + + /** + * Action that gets synced when a post type gets published. + * + * @since 4.4.0 + * + * @param int $post_ID + * @param mixed array $flags post flags that are added to the post + */ + do_action( 'jetpack_published_post', $post_ID, $flags ); + unset( $this->just_published[ $post_ID ] ); + + /** + * Send additional sync action for Activity Log when post is a Customizer publish + */ + if ( 'customize_changeset' == $post->post_type ) { + $post_content = json_decode( $post->post_content, true ); + foreach ( $post_content as $key => $value ) { + // Skip if it isn't a widget + if ( 'widget_' != substr( $key, 0, strlen( 'widget_' ) ) ) { + continue; + } + // Change key from "widget_archives[2]" to "archives-2" + $key = str_replace( 'widget_', '', $key ); + $key = str_replace( '[', '-', $key ); + $key = str_replace( ']', '', $key ); + + global $wp_registered_widgets; + if ( isset( $wp_registered_widgets[ $key ] ) ) { + $widget_data = array( + 'name' => $wp_registered_widgets[ $key ]['name'], + 'id' => $key, + 'title' => $value['value']['title'], + ); + do_action( 'jetpack_widget_edited', $widget_data ); + } + } + } + } + + public function expand_post_ids( $args ) { + list( $post_ids, $previous_interval_end) = $args; + + $posts = array_filter( array_map( array( 'WP_Post', 'get_instance' ), $post_ids ) ); + $posts = array_map( array( $this, 'filter_post_content_and_add_links' ), $posts ); + $posts = array_values( $posts ); // reindex in case posts were deleted + + return array( + $posts, + $this->get_metadata( $post_ids, 'post', Jetpack_Sync_Settings::get_setting( 'post_meta_whitelist' ) ), + $this->get_term_relationships( $post_ids ), + $previous_interval_end + ); + } +} 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..a8dcd43e --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-protect.php @@ -0,0 +1,23 @@ +<?php + +/** + * logs bruteprotect failed logins via sync + */ +class Jetpack_Sync_Module_Protect extends Jetpack_Sync_Module { + + function name() { + return 'protect'; + } + + function init_listeners( $callback ) { + add_action( 'jpp_log_failed_attempt', array( $this, 'maybe_log_failed_login_attempt' ) ); + add_action( 'jetpack_valid_failed_login_attempt', $callback ); + } + + function maybe_log_failed_login_attempt( $failed_attempt ) { + $protect = Jetpack_Protect_Module::instance(); + if ( $protect->has_login_ability() && ! Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) ) { + do_action( 'jetpack_valid_failed_login_attempt', $failed_attempt ); + } + } +} 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..d23d3e00 --- /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, 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..f09371c6 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-terms.php @@ -0,0 +1,114 @@ +<?php + +class Jetpack_Sync_Module_Terms extends Jetpack_Sync_Module { + private $taxonomy_whitelist; + + function name() { + return 'terms'; + } + + function init_listeners( $callable ) { + add_action( 'created_term', array( $this, 'save_term_handler' ), 10, 3 ); + add_action( 'edited_term', array( $this, 'save_term_handler' ), 10, 3 ); + add_action( 'jetpack_sync_save_term', $callable ); + add_action( 'jetpack_sync_add_term', $callable ); + add_action( 'delete_term', $callable, 10, 4 ); + add_action( 'set_object_terms', $callable, 10, 6 ); + add_action( 'deleted_term_relationships', $callable, 10, 2 ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_terms', $callable, 10, 2 ); + } + + function init_before_send() { + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_terms', array( $this, 'expand_term_taxonomy_id' ) ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_terms', $wpdb->term_taxonomy, 'term_taxonomy_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + private function get_where_sql( $config ) { + if ( is_array( $config ) ) { + return 'term_taxonomy_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return ''; + } + + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->term_taxonomy"; + + 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 ); + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_terms' ); + } + + function save_term_handler( $term_id, $tt_id, $taxonomy ) { + if ( class_exists( 'WP_Term' ) ) { + $term_object = WP_Term::get_instance( $term_id, $taxonomy ); + } else { + $term_object = get_term_by( 'id', $term_id, $taxonomy ); + } + + $current_filter = current_filter(); + + if ( 'created_term' === $current_filter ) { + /** + * Fires when the client needs to add a new term + * + * @since 5.0.0 + * + * @param object the Term object + */ + do_action( 'jetpack_sync_add_term', $term_object ); + return; + } + + /** + * Fires when the client needs to update a term + * + * @since 4.2.0 + * + * @param object the Term object + */ + do_action( 'jetpack_sync_save_term', $term_object ); + } + + function set_taxonomy_whitelist( $taxonomies ) { + $this->taxonomy_whitelist = $taxonomies; + } + + function set_defaults() { + $this->taxonomy_whitelist = Jetpack_Sync_Defaults::$default_taxonomy_whitelist; + } + + public function expand_term_taxonomy_id( $args ) { + list( $term_taxonomy_ids, $previous_end ) = $args; + + return + array( 'terms' => get_terms( + array( + 'hide_empty' => false, + 'term_taxonomy_id' => $term_taxonomy_ids, + 'orderby' => 'term_taxonomy_id', + 'order' => 'DESC' + ) + ), + 'previous_end' => $previous_end + ); + } +} 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..e8b4185d --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-themes.php @@ -0,0 +1,612 @@ +<?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' ), 10, 3 ); + add_action( 'jetpack_sync_current_theme_support', $callable, 10, 2 ); + add_action( 'upgrader_process_complete', array( $this, 'check_upgrader' ), 10, 2 ); + add_action( 'jetpack_installed_theme', $callable, 10, 2 ); + add_action( 'jetpack_updated_themes', $callable, 10, 2 ); + add_action( 'delete_site_transient_update_themes', array( $this, 'detect_theme_deletion' ) ); + add_action( 'jetpack_deleted_theme', $callable, 10, 2 ); + add_filter( 'wp_redirect', array( $this, 'detect_theme_edit' ) ); + add_action( 'jetpack_edited_theme', $callable, 10, 2 ); + add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'theme_edit_ajax' ), 0 ); + add_action( 'update_site_option_allowedthemes', array( $this, 'sync_network_allowed_themes_change' ), 10, 4 ); + add_action( 'jetpack_network_disabled_themes', $callable, 10, 2 ); + add_action( 'jetpack_network_enabled_themes', $callable, 10, 2 ); + + // Sidebar updates. + add_action( 'update_option_sidebars_widgets', array( $this, 'sync_sidebar_widgets_actions' ), 10, 2 ); + + add_action( 'jetpack_widget_added', $callable, 10, 4 ); + add_action( 'jetpack_widget_removed', $callable, 10, 4 ); + add_action( 'jetpack_widget_moved_to_inactive', $callable, 10, 2 ); + add_action( 'jetpack_cleared_inactive_widgets', $callable ); + add_action( 'jetpack_widget_reordered', $callable, 10, 2 ); + add_filter( 'widget_update_callback', array( $this, 'sync_widget_edit' ), 10, 4 ); + add_action( 'jetpack_widget_edited', $callable ); + } + + public function sync_widget_edit( $instance, $new_instance, $old_instance, $widget_object ) { + if ( empty( $old_instance ) ) { + return $instance; + } + + // Don't trigger sync action if this is an ajax request, because Customizer makes them during preview before saving changes + if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['customized'] ) ) { + return $instance; + } + + $widget = array( + 'name' => $widget_object->name, + 'id' => $widget_object->id, + 'title' => isset( $new_instance['title'] ) ? $new_instance['title'] : '', + ); + /** + * Trigger action to alert $callable sync listener that a widget was edited + * + * @since 5.0.0 + * + * @param string $widget_name , Name of edited widget + */ + do_action( 'jetpack_widget_edited', $widget ); + + return $instance; + } + + public function sync_network_allowed_themes_change( $option, $value, $old_value, $network_id ) { + $all_enabled_theme_slugs = array_keys( $value ); + + if ( count( $old_value ) > count( $value ) ) { + + // Suppress jetpack_network_disabled_themes sync action when theme is deleted + $delete_theme_call = $this->get_delete_theme_call(); + if ( ! empty( $delete_theme_call ) ) { + return; + } + + $newly_disabled_theme_names = array_keys( array_diff_key( $old_value, $value ) ); + $newly_disabled_themes = $this->get_theme_details_for_slugs( $newly_disabled_theme_names ); + /** + * Trigger action to alert $callable sync listener that network themes were disabled + * + * @since 5.0.0 + * + * @param mixed $newly_disabled_themes, Array of info about network disabled themes + * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes + */ + do_action( 'jetpack_network_disabled_themes', $newly_disabled_themes, $all_enabled_theme_slugs ); + return; + } + + $newly_enabled_theme_names = array_keys( array_diff_key( $value, $old_value ) ); + $newly_enabled_themes = $this->get_theme_details_for_slugs( $newly_enabled_theme_names ); + /** + * Trigger action to alert $callable sync listener that network themes were enabled + * + * @since 5.0.0 + * + * @param mixed $newly_enabled_themes , Array of info about network enabled themes + * @param mixed $all_enabled_theme_slugs, Array of slugs of all enabled themes + */ + do_action( 'jetpack_network_enabled_themes', $newly_enabled_themes, $all_enabled_theme_slugs ); + } + + private function get_theme_details_for_slugs( $theme_slugs ) { + $theme_data = array(); + foreach ( $theme_slugs as $slug ) { + $theme = wp_get_theme( $slug ); + $theme_data[ $slug ] = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'slug' => $slug, + ); + } + return $theme_data; + } + + public function detect_theme_edit( $redirect_url ) { + $url = wp_parse_url( admin_url( $redirect_url ) ); + $theme_editor_url = wp_parse_url( admin_url( 'theme-editor.php' ) ); + + if ( $theme_editor_url['path'] !== $url['path'] ) { + return $redirect_url; + } + + $query_params = array(); + wp_parse_str( $url['query'], $query_params ); + if ( + ! isset( $_POST['newcontent'] ) || + ! isset( $query_params['file'] ) || + ! isset( $query_params['theme'] ) || + ! isset( $query_params['updated'] ) + ) { + return $redirect_url; + } + $theme = wp_get_theme( $query_params['theme'] ); + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * Trigger action to alert $callable sync listener that a theme was edited + * + * @since 5.0.0 + * + * @param string $query_params['theme'], Slug of edited theme + * @param string $theme_data, Information about edited them + */ + do_action( 'jetpack_edited_theme', $query_params['theme'], $theme_data ); + + return $redirect_url; + } + + public function theme_edit_ajax() { + $args = wp_unslash( $_POST ); + + if ( empty( $args['theme'] ) ) { + return; + } + + if ( empty( $args['file'] ) ) { + return; + } + $file = $args['file']; + if ( 0 !== validate_file( $file ) ) { + return; + } + + if ( ! isset( $args['newcontent'] ) ) { + return; + } + + if ( ! isset( $args['nonce'] ) ) { + return; + } + + $stylesheet = $args['theme']; + if ( 0 !== validate_file( $stylesheet ) ) { + return; + } + + if ( ! current_user_can( 'edit_themes' ) ) { + return; + } + + $theme = wp_get_theme( $stylesheet ); + if ( ! $theme->exists() ) { + return; + } + + $real_file = $theme->get_stylesheet_directory() . '/' . $file; + if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $real_file . $stylesheet ) ) { + return; + } + + if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) { + return; + } + + $editable_extensions = wp_get_theme_file_editable_extensions( $theme ); + + $allowed_files = array(); + foreach ( $editable_extensions as $type ) { + switch ( $type ) { + case 'php': + $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) ); + break; + case 'css': + $style_files = $theme->get_files( 'css', -1 ); + $allowed_files['style.css'] = $style_files['style.css']; + $allowed_files = array_merge( $allowed_files, $style_files ); + break; + default: + $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) ); + break; + } + } + + if ( 0 !== validate_file( $real_file, $allowed_files ) ) { + return; + } + + // Ensure file is real. + if ( ! is_file( $real_file ) ) { + return; + } + + // Ensure file extension is allowed. + $extension = null; + if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) { + $extension = strtolower( $matches[1] ); + if ( ! in_array( $extension, $editable_extensions, true ) ) { + return; + } + } + + if ( ! is_writeable( $real_file ) ) { + return; + } + + $file_pointer = fopen( $real_file, 'w+' ); + if ( false === $file_pointer ) { + return; + } + fclose( $file_pointer ); + + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * This action is documented already in this file + */ + do_action( 'jetpack_edited_theme', $stylesheet, $theme_data ); + + } + + public function detect_theme_deletion() { + $delete_theme_call = $this->get_delete_theme_call(); + if ( empty( $delete_theme_call ) ) { + return; + } + + $slug = $delete_theme_call['args'][0]; + $theme = wp_get_theme( $slug ); + $theme_data = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'slug' => $slug, + ); + + /** + * Signals to the sync listener that a theme was deleted and a sync action + * reflecting the deletion and theme slug should be sent + * + * @since 5.0.0 + * + * @param string $slug Theme slug + * @param array $theme_data Theme info Since 5.3 + */ + do_action( 'jetpack_deleted_theme', $slug, $theme_data ); + } + + public function check_upgrader( $upgrader, $details ) { + if ( ! isset( $details['type'] ) || + 'theme' !== $details['type'] || + is_wp_error( $upgrader->skin->result ) || + ! method_exists( $upgrader, 'theme_info' ) + ) { + return; + } + + if ( 'install' === $details['action'] ) { + $theme = $upgrader->theme_info(); + if ( ! $theme instanceof WP_Theme ) { + return; + } + $theme_info = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + ); + + /** + * Signals to the sync listener that a theme was installed and a sync action + * reflecting the installation and the theme info should be sent + * + * @since 4.9.0 + * + * @param string $theme->theme_root Text domain of the theme + * @param mixed $theme_info Array of abbreviated theme info + */ + do_action( 'jetpack_installed_theme', $theme->stylesheet, $theme_info ); + } + + if ( 'update' === $details['action'] ) { + $themes = array(); + + if ( empty( $details['themes'] ) && isset( $details['theme'] ) ) { + $details['themes'] = array( $details['theme'] ); + } + + foreach ( $details['themes'] as $theme_slug ) { + $theme = wp_get_theme( $theme_slug ); + + if ( ! $theme instanceof WP_Theme ) { + continue; + } + + $themes[ $theme_slug ] = array( + 'name' => $theme->get( 'Name' ), + 'version' => $theme->get( 'Version' ), + 'uri' => $theme->get( 'ThemeURI' ), + 'stylesheet' => $theme->stylesheet, + ); + } + + if ( empty( $themes ) ) { + return; + } + + /** + * Signals to the sync listener that one or more themes was updated and a sync action + * reflecting the update and the theme info should be sent + * + * @since 6.2.0 + * + * @param mixed $themes Array of abbreviated theme info + */ + do_action( 'jetpack_updated_themes', $themes ); + } + + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_theme_data', $callable ); + } + + public function sync_theme_support( $new_name, $new_theme = null, $old_theme = null ) { + $previous_theme = $this->get_theme_support_info( $old_theme ); + + /** + * 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 array the theme support array + * @param array the previous theme since Jetpack 6.5.0 + */ + do_action( 'jetpack_sync_current_theme_support', $this->get_theme_support_info(), $previous_theme ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + /** + * Tells the client to sync all theme data to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand theme data (should always be true) + */ + do_action( 'jetpack_full_sync_theme_data', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + public function estimate_full_sync_actions( $config ) { + return 1; + } + + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_theme_data', array( $this, 'expand_theme_data' ) ); + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_theme_data' ); + } + + function expand_theme_data() { + return array( $this->get_theme_support_info() ); + } + + function get_widget_name( $widget_id ) { + global $wp_registered_widgets; + return ( isset( $wp_registered_widgets[ $widget_id ] ) ? $wp_registered_widgets[ $widget_id ]['name'] : null ); + } + + function get_sidebar_name( $sidebar_id ) { + global $wp_registered_sidebars; + return ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : null ); + } + + function sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ) { + $added_widgets = array_diff( $new_widgets, $old_widgets ); + if ( empty( $added_widgets ) ) { + return array(); + } + $moved_to_sidebar = array(); + $sidebar_name = $this->get_sidebar_name( $sidebar ); + + // Don't sync jetpack_widget_added if theme was switched + if ( $this->is_theme_switch() ) { + return array(); + } + + foreach ( $added_widgets as $added_widget ) { + $moved_to_sidebar[] = $added_widget; + $added_widget_name = $this->get_widget_name( $added_widget ); + /** + * Helps Sync log that a widget got added + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $added_widget, Widget id got added + * @param string $sidebar_name, Sidebar id got changed Since 5.0.0 + * @param string $added_widget_name, Widget id got added Since 5.0.0 + */ + do_action( 'jetpack_widget_added', $sidebar, $added_widget, $sidebar_name, $added_widget_name ); + } + return $moved_to_sidebar; + } + + function sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $inactive_widgets ) { + $removed_widgets = array_diff( $old_widgets, $new_widgets ); + + if ( empty( $removed_widgets ) ) { + return array(); + } + + $moved_to_inactive = array(); + $sidebar_name = $this->get_sidebar_name( $sidebar ); + + foreach ( $removed_widgets as $removed_widget ) { + // Lets check if we didn't move the widget to in_active_widgets + if ( isset( $inactive_widgets ) && ! in_array( $removed_widget, $inactive_widgets ) ) { + $removed_widget_name = $this->get_widget_name( $removed_widget ); + /** + * Helps Sync log that a widgte got removed + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $removed_widget, Widget id got removed + * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0 + * @param string $removed_widget_name, Name of the widget that got removed Since 5.0.0 + */ + do_action( 'jetpack_widget_removed', $sidebar, $removed_widget, $sidebar_name, $removed_widget_name ); + } else { + $moved_to_inactive[] = $removed_widget; + } + } + return $moved_to_inactive; + + } + + function sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ) { + $added_widgets = array_diff( $new_widgets, $old_widgets ); + if ( ! empty( $added_widgets ) ) { + return; + } + $removed_widgets = array_diff( $old_widgets, $new_widgets ); + if ( ! empty( $removed_widgets ) ) { + return; + } + + if ( serialize( $old_widgets ) !== serialize( $new_widgets ) ) { + $sidebar_name = $this->get_sidebar_name( $sidebar ); + /** + * Helps Sync log that a sidebar id got reordered + * + * @since 4.9.0 + * + * @param string $sidebar, Sidebar id got changed + * @param string $sidebar_name, Name of the sidebar that changed Since 5.0.0 + */ + do_action( 'jetpack_widget_reordered', $sidebar, $sidebar_name ); + } + + } + + function sync_sidebar_widgets_actions( $old_value, $new_value ) { + // Don't really know how to deal with different array_values yet. + if ( + ( isset( $old_value['array_version'] ) && $old_value['array_version'] !== 3 ) || + ( isset( $new_value['array_version'] ) && $new_value['array_version'] !== 3 ) + ) { + return; + } + + $moved_to_inactive_ids = array(); + $moved_to_sidebar = array(); + + foreach ( $new_value as $sidebar => $new_widgets ) { + if ( in_array( $sidebar, array( 'array_version', 'wp_inactive_widgets' ) ) ) { + continue; + } + $old_widgets = isset( $old_value[ $sidebar ] ) + ? $old_value[ $sidebar ] + : array(); + + if ( ! is_array( $new_widgets ) ) { + $new_widgets = array(); + } + + $moved_to_inactive_recently = $this->sync_remove_widgets_from_sidebar( $new_widgets, $old_widgets, $sidebar, $new_value['wp_inactive_widgets'] ); + $moved_to_inactive_ids = array_merge( $moved_to_inactive_ids, $moved_to_inactive_recently ); + + $moved_to_sidebar_recently = $this->sync_add_widgets_to_sidebar( $new_widgets, $old_widgets, $sidebar ); + $moved_to_sidebar = array_merge( $moved_to_sidebar, $moved_to_sidebar_recently ); + + $this->sync_widgets_reordered( $new_widgets, $old_widgets, $sidebar ); + + } + + // Don't sync either jetpack_widget_moved_to_inactive or jetpack_cleared_inactive_widgets if theme was switched + if ( $this->is_theme_switch() ) { + return; + } + + // Treat inactive sidebar a bit differently + if ( ! empty( $moved_to_inactive_ids ) ) { + $moved_to_inactive_name = array_map( array( $this, 'get_widget_name' ), $moved_to_inactive_ids ); + /** + * Helps Sync log that a widgets IDs got moved to in active + * + * @since 4.9.0 + * + * @param array $moved_to_inactive_ids, Array of widgets id that moved to inactive id got changed + * @param array $moved_to_inactive_names, Array of widgets names that moved to inactive id got changed Since 5.0.0 + */ + do_action( 'jetpack_widget_moved_to_inactive', $moved_to_inactive_ids, $moved_to_inactive_name ); + } elseif ( empty( $moved_to_sidebar ) && + empty( $new_value['wp_inactive_widgets'] ) && + ! empty( $old_value['wp_inactive_widgets'] ) ) { + /** + * Helps Sync log that a got cleared from inactive. + * + * @since 4.9.0 + */ + do_action( 'jetpack_cleared_inactive_widgets' ); + } + } + + /** + * @param null $theme or the theme object + * + * @return array + */ + private function get_theme_support_info( $theme = null ) { + global $_wp_theme_features; + + $theme_support = array(); + + // We are trying to get the current theme info. + if ( $theme === null ) { + $theme = wp_get_theme(); + + foreach ( Jetpack_Sync_Defaults::$default_theme_support_whitelist as $theme_feature ) { + $has_support = current_theme_supports( $theme_feature ); + if ( $has_support ) { + $theme_support[ $theme_feature ] = $_wp_theme_features[ $theme_feature ]; + } + } + } + + $theme_support['name'] = $theme->get( 'Name' ); + $theme_support['version'] = $theme->get( 'Version' ); + $theme_support['slug'] = $theme->get_stylesheet(); + $theme_support['uri'] = $theme->get( 'ThemeURI' ); + + return $theme_support; + } + + private function get_delete_theme_call() { + $backtrace = debug_backtrace(); + $delete_theme_call = null; + foreach ( $backtrace as $call ) { + if ( isset( $call['function'] ) && 'delete_theme' === $call['function'] ) { + $delete_theme_call = $call; + break; + } + } + return $delete_theme_call; + } + + private function is_theme_switch() { + return did_action( 'after_switch_theme' ); + } +} 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..4da4414f --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-updates.php @@ -0,0 +1,321 @@ +<?php + +class Jetpack_Sync_Module_Updates extends Jetpack_Sync_Module { + + const UPDATES_CHECKSUM_OPTION_NAME = 'jetpack_updates_sync_checksum'; + + private $old_wp_version = null; + private $updates = array(); + + public function set_defaults() { + $this->updates = array(); + } + + function name() { + return 'updates'; + } + + public function init_listeners( $callable ) { + global $wp_version; + $this->old_wp_version = $wp_version; + add_action( 'set_site_transient_update_plugins', array( $this, 'validate_update_change' ), 10, 3 ); + add_action( 'set_site_transient_update_themes', array( $this, 'validate_update_change' ), 10, 3 ); + add_action( 'set_site_transient_update_core', array( $this, 'validate_update_change' ), 10, 3 ); + + add_action( 'jetpack_update_plugins_change', $callable ); + add_action( 'jetpack_update_themes_change', $callable ); + add_action( 'jetpack_update_core_change', $callable ); + + add_filter( + 'jetpack_sync_before_enqueue_jetpack_update_plugins_change', + array( + $this, + 'filter_update_keys', + ), + 10, + 2 + ); + add_filter( + 'jetpack_sync_before_enqueue_upgrader_process_complete', + array( + $this, + 'filter_upgrader_process_complete', + ), + 10, + 2 + ); + + add_action( 'automatic_updates_complete', $callable ); + + if ( is_multisite() ) { + add_filter( 'pre_update_site_option_wpmu_upgrade_site', array( $this, 'update_core_network_event' ), 10, 2 ); + add_action( 'jetpack_sync_core_update_network', $callable, 10, 3 ); + } + + // Send data when update completes + add_action( '_core_updated_successfully', array( $this, 'update_core' ) ); + add_action( 'jetpack_sync_core_reinstalled_successfully', $callable ); + add_action( 'jetpack_sync_core_autoupdated_successfully', $callable, 10, 2 ); + add_action( 'jetpack_sync_core_updated_successfully', $callable, 10, 2 ); + + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_updates', $callable ); + } + + public function init_before_send() { + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) ); + add_filter( 'jetpack_sync_before_send_jetpack_update_themes_change', array( $this, 'expand_themes' ) ); + } + + public function update_core_network_event( $wp_db_version, $old_wp_db_version ) { + global $wp_version; + /** + * Sync event for when core wp network updates to a new db version + * + * @since 5.0.0 + * + * @param int $wp_db_version the latest wp_db_version + * @param int $old_wp_db_version previous wp_db_version + * @param string $wp_version the latest wp_version + */ + do_action( 'jetpack_sync_core_update_network', $wp_db_version, $old_wp_db_version, $wp_version ); + return $wp_db_version; + } + + public function update_core( $new_wp_version ) { + global $pagenow; + + if ( isset( $_GET['action'] ) && 'do-core-reinstall' === $_GET['action'] ) { + /** + * Sync event that fires when core reinstall was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + */ + do_action( 'jetpack_sync_core_reinstalled_successfully', $new_wp_version ); + return; + } + + // Core was autoudpated + if ( + 'update-core.php' !== $pagenow && + ! Jetpack_Constants::is_true( 'REST_API_REQUEST' ) // wp.com rest api calls should never be marked as a core autoupdate + ) { + /** + * Sync event that fires when core autoupdate was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + * @param string $old_wp_version the previous WordPress version + */ + do_action( 'jetpack_sync_core_autoupdated_successfully', $new_wp_version, $this->old_wp_version ); + return; + } + /** + * Sync event that fires when core update was successful + * + * @since 5.0.0 + * + * @param string $new_wp_version the updated WordPress version + * @param string $old_wp_version the previous WordPress version + */ + do_action( 'jetpack_sync_core_updated_successfully', $new_wp_version, $this->old_wp_version ); + return; + + } + + public function get_update_checksum( $update, $transient ) { + $updates = array(); + $no_updated = array(); + switch ( $transient ) { + case 'update_plugins': + if ( ! empty( $update->response ) && is_array( $update->response ) ) { + foreach ( $update->response as $plugin_slug => $response ) { + if ( ! empty( $plugin_slug ) && isset( $response->new_version ) ) { + $updates[] = array( $plugin_slug => $response->new_version ); + } + } + } + if ( ! empty( $update->no_update ) ) { + $no_updated = array_keys( $update->no_update ); + } + + if ( ! isset( $no_updated['jetpack/jetpack.php'] ) && isset( $updates['jetpack/jetpack.php'] ) ) { + return false; + } + + break; + case 'update_themes': + if ( ! empty( $update->response ) && is_array( $update->response ) ) { + foreach ( $update->response as $theme_slug => $response ) { + if ( ! empty( $theme_slug ) && isset( $response['new_version'] ) ) { + $updates[] = array( $theme_slug => $response['new_version'] ); + } + } + } + + if ( ! empty( $update->checked ) ) { + $no_updated = $update->checked; + } + + break; + case 'update_core': + if ( ! empty( $update->updates ) && is_array( $update->updates ) ) { + foreach ( $update->updates as $response ) { + if ( ! empty( $response->response ) && $response->response === 'latest' ) { + continue; + } + if ( ! empty( $response->response ) && isset( $response->packages->full ) ) { + $updates[] = array( $response->response => $response->packages->full ); + } + } + } + + if ( ! empty( $update->version_checked ) ) { + $no_updated = $update->version_checked; + } + + if ( empty( $updates ) ) { + return false; + } + break; + + } + if ( empty( $updates ) && empty( $no_updated ) ) { + return false; + } + return $this->get_check_sum( array( $no_updated, $updates ) ); + } + + public function validate_update_change( $value, $expiration, $transient ) { + $new_checksum = $this->get_update_checksum( $value, $transient ); + + if ( false === $new_checksum ) { + return; + } + + $checksums = get_option( self::UPDATES_CHECKSUM_OPTION_NAME, array() ); + + if ( isset( $checksums[ $transient ] ) && $checksums[ $transient ] === $new_checksum ) { + return; + } + + $checksums[ $transient ] = $new_checksum; + + update_option( self::UPDATES_CHECKSUM_OPTION_NAME, $checksums ); + if ( 'update_core' === $transient ) { + /** + * jetpack_update_core_change + * + * @since 5.1.0 + * + * @param array containing info that tells us what needs updating + */ + do_action( 'jetpack_update_core_change', $value ); + return; + } + if ( empty( $this->updates ) ) { + // lets add the shutdown method once and only when the updates move from empty to filled with something + add_action( 'shutdown', array( $this, 'sync_last_event' ), 9 ); + } + if ( ! isset( $this->updates[ $transient ] ) ) { + $this->updates[ $transient ] = array(); + } + $this->updates[ $transient ][] = $value; + } + + public function sync_last_event() { + foreach ( $this->updates as $transient => $values ) { + $value = end( $values ); // only send over the last value + /** + * jetpack_{$transient}_change + * jetpack_update_plugins_change + * jetpack_update_themes_change + * + * @since 5.1.0 + * + * @param array containing info that tells us what needs updating + */ + do_action( "jetpack_{$transient}_change", $value ); + } + + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + /** + * Tells the client to sync all updates to the server + * + * @since 4.2.0 + * + * @param boolean Whether to expand updates (should always be true) + */ + do_action( 'jetpack_full_sync_updates', true ); + + // The number of actions enqueued, and next module state (true == done) + return array( 1, true ); + } + + public function estimate_full_sync_actions( $config ) { + return 1; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_updates' ); + } + + public function get_all_updates() { + return array( + 'core' => get_site_transient( 'update_core' ), + 'plugins' => get_site_transient( 'update_plugins' ), + 'themes' => get_site_transient( 'update_themes' ), + ); + } + + // removes unnecessary keys from synced updates data + function filter_update_keys( $args ) { + $updates = $args[0]; + + if ( isset( $updates->no_update ) ) { + unset( $updates->no_update ); + } + + return $args; + } + + function filter_upgrader_process_complete( $args ) { + array_shift( $args ); + + return $args; + } + + public function expand_updates( $args ) { + if ( $args[0] ) { + return $this->get_all_updates(); + } + + return $args; + } + + public function expand_themes( $args ) { + if ( ! isset( $args[0], $args[0]->response ) ) { + return $args; + } + if ( ! is_array( $args[0]->response ) ) { + trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response ) . ' instead', E_USER_WARNING ); + return $args; + } + foreach ( $args[0]->response as $stylesheet => &$theme_data ) { + $theme = wp_get_theme( $stylesheet ); + $theme_data['name'] = $theme->name; + } + return $args; + } + + public function reset_data() { + delete_option( self::UPDATES_CHECKSUM_OPTION_NAME ); + } +} 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..e74d7116 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-users.php @@ -0,0 +1,500 @@ +<?php + +class Jetpack_Sync_Module_Users extends Jetpack_Sync_Module { + const MAX_INITIAL_SYNC_USERS = 100; + + protected $flags = array(); + + function name() { + return 'users'; + } + + // this is here to support the backfill API + public function get_object_by_id( $object_type, $id ) { + if ( $object_type === 'user' && $user = get_user_by( 'id', intval( $id ) ) ) { + return $this->sanitize_user_and_expand( $user ); + } + + return false; + } + + public function init_listeners( $callable ) { + + // users + add_action( 'user_register', array( $this, 'user_register_handler' ) ); + add_action( 'profile_update', array( $this, 'save_user_handler' ), 10, 2 ); + + add_action( 'add_user_to_blog', array( $this, 'add_user_to_blog_handler' ) ); + add_action( 'jetpack_sync_add_user', $callable, 10, 2 ); + + add_action( 'jetpack_sync_register_user', $callable, 10, 2 ); + add_action( 'jetpack_sync_save_user', $callable, 10, 2 ); + + add_action( 'jetpack_sync_user_locale', $callable, 10, 2 ); + add_action( 'jetpack_sync_user_locale_delete', $callable, 10, 1 ); + + add_action( 'deleted_user', array( $this, 'deleted_user_handler' ), 10, 2 ); + add_action( 'jetpack_deleted_user', $callable, 10, 3 ); + add_action( 'remove_user_from_blog', array( $this, 'remove_user_from_blog_handler' ), 10, 2 ); + add_action( 'jetpack_removed_user_from_blog', $callable, 10, 2 ); + + // user roles + add_action( 'add_user_role', array( $this, 'save_user_role_handler' ), 10, 2 ); + add_action( 'set_user_role', array( $this, 'save_user_role_handler' ), 10, 3 ); + add_action( 'remove_user_role', array( $this, 'save_user_role_handler' ), 10, 2 ); + + // user capabilities + add_action( 'added_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + add_action( 'updated_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + add_action( 'deleted_user_meta', array( $this, 'maybe_save_user_meta' ), 10, 4 ); + + // user authentication + add_filter( 'authenticate', array( $this, 'authenticate_handler' ), 1000, 3 ); + add_action( 'wp_login', array( $this, 'wp_login_handler' ), 10, 2 ); + + add_action( 'jetpack_wp_login', $callable, 10, 3 ); + + add_action( 'wp_logout', $callable, 10, 0 ); + + // Add on init + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_add_user', array( $this, 'expand_action' ) ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_register_user', array( $this, 'expand_action' ) ); + add_filter( 'jetpack_sync_before_enqueue_jetpack_sync_save_user', array( $this, 'expand_action' ) ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_users', $callable ); + } + + public function init_before_send() { + + add_filter( 'jetpack_sync_before_send_jetpack_wp_login', array( $this, 'expand_login_username' ), 10, 1 ); + add_filter( 'jetpack_sync_before_send_wp_logout', array( $this, 'expand_logout_username' ), 10, 2 ); + + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) ); + } + + private function get_user( $user ) { + if ( is_numeric( $user ) ) { + $user = get_user_by( 'id', $user ); + } + if ( $user instanceof WP_User ) { + return $user; + } + return null; + } + + public function sanitize_user( $user ) { + $user = $this->get_user( $user ); + // this create a new user object and stops the passing of the object by reference. + $user = unserialize( serialize( $user ) ); + + if ( is_object( $user ) && is_object( $user->data ) ) { + unset( $user->data->user_pass ); + } + return $user; + } + + public function expand_user( $user ) { + if ( ! is_object( $user ) ) { + return null; + } + $user->allowed_mime_types = get_allowed_mime_types( $user ); + $user->allcaps = $this->get_real_user_capabilities( $user ); + + // Only set the user locale if it is different from the site local + if ( get_locale() !== get_user_locale( $user->ID ) ) { + $user->locale = get_user_locale( $user->ID ); + } + + return $user; + } + + public function get_real_user_capabilities( $user ) { + $user_capabilities = array(); + if ( is_wp_error( $user ) ) { + return $user_capabilities; + } + foreach ( Jetpack_Sync_Defaults::get_capabilities_whitelist() as $capability ) { + if ( $user_has_capabilities = user_can( $user, $capability ) ) { + $user_capabilities[ $capability ] = true; + } + } + return $user_capabilities; + } + + public function sanitize_user_and_expand( $user ) { + $user = $this->get_user( $user ); + $user = $this->expand_user( $user ); + return $this->sanitize_user( $user ); + } + + public function expand_action( $args ) { + // the first argument is always the user + list( $user ) = $args; + if ( $user ) { + $args[0] = $this->sanitize_user_and_expand( $user ); + return $args; + } + + return false; + } + + public function expand_login_username( $args ) { + list( $login, $user, $flags ) = $args; + $user = $this->sanitize_user( $user ); + + return array( $login, $user, $flags ); + } + + public function expand_logout_username( $args, $user_id ) { + $user = get_userdata( $user_id ); + $user = $this->sanitize_user( $user ); + + $login = ''; + if ( is_object( $user ) && is_object( $user->data ) ) { + $login = $user->data->user_login; + } + // if we don't have a user here lets not send anything. + if ( empty( $login ) ) { + return false; + } + + return array( $login, $user ); + } + + /** + * Additional processing is needed for wp_login so we introduce this wrapper + * handler. + * + * @param String $user_login the user login. + * @param WP_User $user the user object. + */ + function wp_login_handler( $user_login, $user ) { + /** + * Fires when a user is logged into a site. + * + * @since 7.2.0 + * + * @param Numeric $user_id The user ID. + * @param WP_User $user The User Object of the user that currently logged in + * @param Array $params Any Flags that have been added during login + */ + do_action( 'jetpack_wp_login', $user->ID, $user, $this->get_flags( $user->ID ) ); + $this->clear_flags( $user->ID ); + } + + /** + * A hook for the authenticate event that checks the password strength. + * + * @param WP_Error|WP_User $user the user object, or an error. + * @param String $username the username. + * @param Sting $password the password used to authenticate. + * @return WP_Error|WP_User the same object that was passed into the function. + */ + public function authenticate_handler( $user, $username, $password ) { + // In case of cookie authentication we don't do anything here. + if ( empty( $password ) ) { + return $user; + } + + // We are only interested in successful authentication events. + if ( is_wp_error( $user ) || ! ( $user instanceof WP_User ) ) { + return $user; + } + + jetpack_require_lib( 'class.jetpack-password-checker' ); + $password_checker = new Jetpack_Password_Checker( $user->ID ); + + $test_results = $password_checker->test( $password, true ); + + // If the password passes tests, we don't do anything. + if ( empty( $test_results['test_results']['failed'] ) ) { + return $user; + } + + $this->add_flags( + $user->ID, + array( + 'warning' => 'The password failed at least one strength test.', + 'failures' => $test_results['test_results']['failed'], + ) + ); + + return $user; + } + + public function deleted_user_handler( $deleted_user_id, $reassigned_user_id = '' ) { + $is_multisite = is_multisite(); + /** + * Fires when a user is deleted on a site + * + * @since 5.4.0 + * + * @param int $deleted_user_id - ID of the deleted user + * @param int $reassigned_user_id - ID of the user the deleted user's posts is reassigned to (if any) + * @param bool $is_multisite - Whether this site is a multisite installation + */ + do_action( 'jetpack_deleted_user', $deleted_user_id, $reassigned_user_id, $is_multisite ); + } + + function user_register_handler( $user_id, $old_user_data = null ) { + // ensure we only sync users who are members of the current blog + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) { + $this->add_flags( $user_id, array( 'invitation_accepted' => true ) ); + } + /** + * Fires when a new user is registered on a site + * + * @since 4.9.0 + * + * @param object The WP_User object + */ + do_action( 'jetpack_sync_register_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + + } + + function add_user_to_blog_handler( $user_id, $old_user_data = null ) { + // ensure we only sync users who are members of the current blog + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + if ( Jetpack_Constants::is_true( 'JETPACK_INVITE_ACCEPTED' ) ) { + $this->add_flags( $user_id, array( 'invitation_accepted' => true ) ); + } + /** + * Fires when a user is added on a site + * + * @since 4.9.0 + * + * @param object The WP_User object + */ + do_action( 'jetpack_sync_add_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + function save_user_handler( $user_id, $old_user_data = null ) { + // ensure we only sync users who are members of the current blog + if ( ! is_user_member_of_blog( $user_id, get_current_blog_id() ) ) { + return; + } + + $user = get_user_by( 'id', $user_id ); + + // Older versions of WP don't pass the old_user_data in ->data + if ( isset( $old_user_data->data ) ) { + $old_user = $old_user_data->data; + } else { + $old_user = $old_user_data; + } + + if ( $old_user !== null && $user->user_pass !== $old_user->user_pass ) { + $this->flags[ $user_id ]['password_changed'] = true; + } + if ( $old_user !== null && $user->data->user_email !== $old_user->user_email ) { + // The '_new_email' user meta is deleted right after the call to wp_update_user + // that got us to this point so if it's still set then this was a user confirming + // their new email address + if ( 1 === intval( get_user_meta( $user->ID, '_new_email', true ) ) ) { + $this->flags[ $user_id ]['email_changed'] = true; + } + } + + /** + * Fires when the client needs to sync an updated user + * + * @since 4.2.0 + * + * @param object The WP_User object + * @param array state - New since 5.8.0 + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + function save_user_role_handler( $user_id, $role, $old_roles = null ) { + $this->add_flags( + $user_id, + array( + 'role_changed' => true, + 'previous_role' => $old_roles, + ) + ); + + // The jetpack_sync_register_user payload is identical to jetpack_sync_save_user, don't send both + if ( $this->is_create_user() || $this->is_add_user_to_blog() ) { + return; + } + /** + * This action is documented already in this file + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + $this->clear_flags( $user_id ); + } + + function get_flags( $user_id ) { + if ( isset( $this->flags[ $user_id ] ) ) { + return $this->flags[ $user_id ]; + } + return array(); + } + + function clear_flags( $user_id ) { + if ( isset( $this->flags[ $user_id ] ) ) { + unset( $this->flags[ $user_id ] ); + } + } + + function add_flags( $user_id, $flags ) { + $this->flags[ $user_id ] = wp_parse_args( $flags, $this->get_flags( $user_id ) ); + } + + function maybe_save_user_meta( $meta_id, $user_id, $meta_key, $value ) { + if ( $meta_key === 'locale' ) { + $this->add_flags( $user_id, array( 'locale_changed' => true ) ); + } + + $user = get_user_by( 'id', $user_id ); + if ( isset( $user->cap_key ) && $meta_key === $user->cap_key ) { + $this->add_flags( $user_id, array( 'capabilities_changed' => true ) ); + } + + if ( $this->is_create_user() || $this->is_add_user_to_blog() || $this->is_delete_user() ) { + return; + } + + if ( isset( $this->flags[ $user_id ] ) ) { + /** + * This action is documented already in this file + */ + do_action( 'jetpack_sync_save_user', $user_id, $this->get_flags( $user_id ) ); + } + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->usermeta, 'user_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $wpdb->usermeta"; + + if ( $where_sql = $this->get_where_sql( $config ) ) { + $query .= ' WHERE ' . $where_sql; + } + + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + private function get_where_sql( $config ) { + global $wpdb; + + $query = "meta_key = '{$wpdb->prefix}capabilities'"; + + // config is a list of user IDs to sync + if ( is_array( $config ) ) { + $query .= ' AND user_id IN (' . implode( ',', array_map( 'intval', $config ) ) . ')'; + } + + return $query; + } + + function get_full_sync_actions() { + return array( 'jetpack_full_sync_users' ); + } + + function get_initial_sync_user_config() { + global $wpdb; + + $user_ids = $wpdb->get_col( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = '{$wpdb->prefix}user_level' AND meta_value > 0 LIMIT " . ( self::MAX_INITIAL_SYNC_USERS + 1 ) ); + + if ( count( $user_ids ) <= self::MAX_INITIAL_SYNC_USERS ) { + return $user_ids; + } else { + return false; + } + } + + public function expand_users( $args ) { + list( $user_ids, $previous_end ) = $args; + + return array( + 'users' => array_map( array( $this, 'sanitize_user_and_expand' ), get_users( + array( + 'include' => $user_ids, + 'orderby' => 'ID', + 'order' => 'DESC' + ) + ) ), + 'previous_end' => $previous_end + ); + } + + public function remove_user_from_blog_handler( $user_id, $blog_id ) { + // User is removed on add, see https://github.com/WordPress/WordPress/blob/0401cee8b36df3def8e807dd766adc02b359dfaf/wp-includes/ms-functions.php#L2114 + if ( $this->is_add_new_user_to_blog() ) { + return; + } + + $reassigned_user_id = $this->get_reassigned_network_user_id(); + + // Note that we are in the context of the blog the user is removed from, see https://github.com/WordPress/WordPress/blob/473e1ba73bc5c18c72d7f288447503713d518790/wp-includes/ms-functions.php#L233 + /** + * Fires when a user is removed from a blog on a multisite installation + * + * @since 5.4.0 + * + * @param int $user_id - ID of the removed user + * @param int $reassigned_user_id - ID of the user the removed user's posts is reassigned to (if any) + */ + do_action( 'jetpack_removed_user_from_blog', $user_id, $reassigned_user_id ); + } + + protected function is_add_new_user_to_blog() { + return Jetpack::is_function_in_backtrace( 'add_new_user_to_blog' ); + } + + protected function is_add_user_to_blog() { + return Jetpack::is_function_in_backtrace( 'add_user_to_blog' ); + } + + protected function is_delete_user() { + return Jetpack::is_function_in_backtrace( array( 'wp_delete_user', 'remove_user_from_blog' ) ); + } + + protected function is_create_user() { + $functions = array( + 'add_new_user_to_blog', // Used to suppress jetpack_sync_save_user in save_user_cap_handler when user registered on multi site + 'wp_create_user', // Used to suppress jetpack_sync_save_user in save_user_role_handler when user registered on multi site + 'wp_insert_user', // Used to suppress jetpack_sync_save_user in save_user_cap_handler and save_user_role_handler when user registered on single site + ); + + return Jetpack::is_function_in_backtrace( $functions ); + } + + protected function get_reassigned_network_user_id() { + $backtrace = debug_backtrace( false ); // phpcs:ignore PHPCompatibility.PHP.NewFunctionParameters.debug_backtrace_optionsFound + foreach ( $backtrace as $call ) { + if ( + 'remove_user_from_blog' === $call['function'] && + 3 === count( $call['args'] ) + ) { + return $call['args'][2]; + } + } + + return false; + } +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-woocommerce.php b/plugins/jetpack/sync/class.jetpack-sync-module-woocommerce.php new file mode 100644 index 00000000..962258a2 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-woocommerce.php @@ -0,0 +1,335 @@ +<?php + +require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-module.php'; + +class Jetpack_Sync_Module_WooCommerce extends Jetpack_Sync_Module { + + private $order_item_meta_whitelist = array( + // https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 + '_product_id', + '_variation_id', + '_qty', + // Tax ones also included in below class + // https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 + '_tax_class', + '_tax_status', + '_line_subtotal', + '_line_subtotal_tax', + '_line_total', + '_line_tax', + '_line_tax_data', + // https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 + 'method_id', + 'cost', + 'total_tax', + 'taxes', + // https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 + 'rate_id', + 'label', + 'compound', + 'tax_amount', + 'shipping_tax_amount', + // https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php + 'discount_amount', + 'discount_amount_tax', + ); + + private $order_item_table_name; + + public function __construct() { + global $wpdb; + $this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items'; + + // options, constants and post meta whitelists + add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 ); + add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 ); + add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 ); + add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 ); + + add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) ); + add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'filter_order_item' ) ); + } + + function name() { + return 'woocommerce'; + } + + public function init_listeners( $callable ) { + // attributes + add_action( 'woocommerce_attribute_added', $callable, 10, 2 ); + add_action( 'woocommerce_attribute_updated', $callable, 10, 3 ); + add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 ); + + // orders + add_action( 'woocommerce_new_order', $callable, 10, 1 ); + add_action( 'woocommerce_order_status_changed', $callable, 10, 3 ); + add_action( 'woocommerce_payment_complete', $callable, 10, 1 ); + + // order items + add_action( 'woocommerce_new_order_item', $callable, 10, 4 ); + add_action( 'woocommerce_update_order_item', $callable, 10, 4 ); + add_action( 'woocommerce_delete_order_item', $callable, 10, 1 ); + $this->init_listeners_for_meta_type( 'order_item', $callable ); + + // payment tokens + add_action( 'woocommerce_new_payment_token', $callable, 10, 1 ); + add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 ); + add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 ); + $this->init_listeners_for_meta_type( 'payment_token', $callable ); + + // product downloads + add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 ); + add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 ); + + // tax rates + add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 ); + add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 ); + add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 ); + + // webhooks + add_action( 'woocommerce_new_webhook', $callable, 10, 1 ); + add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 ); + add_action( 'woocommerce_webhook_updated', $callable, 10, 1 ); + } + + public function init_full_sync_listeners( $callable ) { + add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // also sends post meta + } + + public function get_full_sync_actions() { + return array( 'jetpack_full_sync_woocommerce_order_items' ); + } + + public function init_before_send() { + // full sync + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'expand_order_item_ids' ) ); + } + + public function filter_order_item( $args ) { + // Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details + $args[1] = $this->build_order_item( $args[0] ); + return $args; + } + + public function expand_order_item_ids( $args ) { + $order_item_ids = $args[0]; + + global $wpdb; + + $order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) ); + + $order_items = $wpdb->get_results( + "SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )" + ); + + return array( + $order_items, + $this->get_metadata( $order_item_ids, 'order_item', $this->order_item_meta_whitelist ), + ); + } + + public function build_order_item( $order_item_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) ); + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + global $wpdb; + + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_woocommerce_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state ); + } + + public function estimate_full_sync_actions( $config ) { + global $wpdb; + + $query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config ); + $count = $wpdb->get_var( $query ); + + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE ); + } + + private function get_where_sql( $config ) { + return '1=1'; + } + + public function add_woocommerce_options_whitelist( $list ) { + return array_merge( $list, self::$wc_options_whitelist ); + } + + public function add_woocommerce_constants_whitelist( $list ) { + return array_merge( $list, self::$wc_constants_whitelist ); + } + + public function add_woocommerce_post_meta_whitelist( $list ) { + return array_merge( $list, self::$wc_post_meta_whitelist ); + } + + public function add_woocommerce_comment_meta_whitelist( $list ) { + return array_merge( $list, self::$wc_comment_meta_whitelist ); + } + + private static $wc_options_whitelist = array( + 'woocommerce_currency', + 'woocommerce_db_version', + 'woocommerce_weight_unit', + 'woocommerce_version', + 'woocommerce_unforce_ssl_checkout', + 'woocommerce_tax_total_display', + 'woocommerce_tax_round_at_subtotal', + 'woocommerce_tax_display_shop', + 'woocommerce_tax_display_cart', + 'woocommerce_prices_include_tax', + 'woocommerce_price_thousand_sep', + 'woocommerce_price_num_decimals', + 'woocommerce_price_decimal_sep', + 'woocommerce_notify_low_stock', + 'woocommerce_notify_low_stock_amount', + 'woocommerce_notify_no_stock', + 'woocommerce_notify_no_stock_amount', + 'woocommerce_manage_stock', + 'woocommerce_force_ssl_checkout', + 'woocommerce_hide_out_of_stock_items', + 'woocommerce_file_download_method', + 'woocommerce_enable_signup_and_login_from_checkout', + 'woocommerce_enable_shipping_calc', + 'woocommerce_enable_review_rating', + 'woocommerce_enable_guest_checkout', + 'woocommerce_enable_coupons', + 'woocommerce_enable_checkout_login_reminder', + 'woocommerce_enable_ajax_add_to_cart', + 'woocommerce_dimension_unit', + 'woocommerce_default_country', + 'woocommerce_default_customer_address', + 'woocommerce_currency_pos', + 'woocommerce_api_enabled', + 'woocommerce_allow_tracking', + ); + + private static $wc_constants_whitelist = array( + // woocommerce options + 'WC_PLUGIN_FILE', + 'WC_ABSPATH', + 'WC_PLUGIN_BASENAME', + 'WC_VERSION', + 'WOOCOMMERCE_VERSION', + 'WC_ROUNDING_PRECISION', + 'WC_DISCOUNT_ROUNDING_MODE', + 'WC_TAX_ROUNDING_MODE', + 'WC_DELIMITER', + 'WC_LOG_DIR', + 'WC_SESSION_CACHE_GROUP', + 'WC_TEMPLATE_DEBUG_MODE', + ); + + private static $wc_post_meta_whitelist = array( + // woocommerce products + // https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 + '_visibility', + '_sku', + '_price', + '_regular_price', + '_sale_price', + '_sale_price_dates_from', + '_sale_price_dates_to', + 'total_sales', + '_tax_status', + '_tax_class', + '_manage_stock', + '_backorders', + '_sold_individually', + '_weight', + '_length', + '_width', + '_height', + '_upsell_ids', + '_crosssell_ids', + '_purchase_note', + '_default_attributes', + '_product_attributes', + '_virtual', + '_downloadable', + '_download_limit', + '_download_expiry', + '_featured', + '_downloadable_files', + '_wc_rating_count', + '_wc_average_rating', + '_wc_review_count', + '_variation_description', + '_thumbnail_id', + '_file_paths', + '_product_image_gallery', + '_product_version', + '_wp_old_slug', + + // woocommerce orders + // https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 + '_order_key', + '_order_currency', + // '_billing_first_name', do not sync these as they contain personal data + // '_billing_last_name', + // '_billing_company', + // '_billing_address_1', + // '_billing_address_2', + '_billing_city', + '_billing_state', + '_billing_postcode', + '_billing_country', + // '_billing_email', do not sync these as they contain personal data + // '_billing_phone', + // '_shipping_first_name', + // '_shipping_last_name', + // '_shipping_company', + // '_shipping_address_1', + // '_shipping_address_2', + '_shipping_city', + '_shipping_state', + '_shipping_postcode', + '_shipping_country', + '_completed_date', + '_paid_date', + '_cart_discount', + '_cart_discount_tax', + '_order_shipping', + '_order_shipping_tax', + '_order_tax', + '_order_total', + '_payment_method', + '_payment_method_title', + // '_transaction_id', do not sync these as they contain personal data + // '_customer_ip_address', + // '_customer_user_agent', + '_created_via', + '_order_version', + '_prices_include_tax', + '_date_completed', + '_date_paid', + '_payment_tokens', + '_billing_address_index', + '_shipping_address_index', + '_recorded_sales', + '_recorded_coupon_usage_counts', + // https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 + '_download_permissions_granted', + // https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 + '_order_stock_reduced', + + // woocommerce order refunds + // https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 + '_order_currency', + '_refund_amount', + '_refunded_by', + '_refund_reason', + '_order_shipping', + '_order_shipping_tax', + '_order_tax', + '_order_total', + '_order_version', + '_prices_include_tax', + '_payment_tokens', + ); + + private static $wc_comment_meta_whitelist = array( + 'rating', + ); +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-module-wp-super-cache.php b/plugins/jetpack/sync/class.jetpack-sync-module-wp-super-cache.php new file mode 100644 index 00000000..04d6c5dd --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module-wp-super-cache.php @@ -0,0 +1,79 @@ +<?php + +class Jetpack_Sync_Module_WP_Super_Cache extends Jetpack_Sync_Module { + + public function __construct() { + add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_wp_super_cache_constants_whitelist' ), 10 ); + add_filter( 'jetpack_sync_callable_whitelist', array( $this, 'add_wp_super_cache_callable_whitelist' ), 10 ); + } + + static $wp_super_cache_constants = array( + 'WPLOCKDOWN', + 'WPSC_DISABLE_COMPRESSION', + 'WPSC_DISABLE_LOCKING', + 'WPSC_DISABLE_HTACCESS_UPDATE', + 'ADVANCEDCACHEPROBLEM', + ); + + static $wp_super_cache_callables = array( + 'wp_super_cache_globals' => array( 'Jetpack_Sync_Module_WP_Super_Cache', 'get_wp_super_cache_globals' ), + ); + + public function name() { + return 'wp-super-cache'; + } + + public static function get_wp_super_cache_globals() { + global $wp_cache_mod_rewrite; + global $cache_enabled; + global $super_cache_enabled; + global $ossdlcdn; + global $cache_rebuild_files; + global $wp_cache_mobile; + global $wp_super_cache_late_init; + global $wp_cache_anon_only; + global $wp_cache_not_logged_in; + global $wp_cache_clear_on_post_edit; + global $wp_cache_mobile_enabled; + global $wp_super_cache_debug; + global $cache_max_time; + global $wp_cache_refresh_single_only; + global $wp_cache_mfunc_enabled; + global $wp_supercache_304; + global $wp_cache_no_cache_for_get; + global $wp_cache_mutex_disabled; + global $cache_jetpack; + global $cache_domain_mapping; + + return array( + 'wp_cache_mod_rewrite' => $wp_cache_mod_rewrite, + 'cache_enabled' => $cache_enabled, + 'super_cache_enabled' => $super_cache_enabled, + 'ossdlcdn' => $ossdlcdn, + 'cache_rebuild_files' => $cache_rebuild_files, + 'wp_cache_mobile' => $wp_cache_mobile, + 'wp_super_cache_late_init' => $wp_super_cache_late_init, + 'wp_cache_anon_only' => $wp_cache_anon_only, + 'wp_cache_not_logged_in' => $wp_cache_not_logged_in, + 'wp_cache_clear_on_post_edit' => $wp_cache_clear_on_post_edit, + 'wp_cache_mobile_enabled' => $wp_cache_mobile_enabled, + 'wp_super_cache_debug' => $wp_super_cache_debug, + 'cache_max_time' => $cache_max_time, + 'wp_cache_refresh_single_only' => $wp_cache_refresh_single_only, + 'wp_cache_mfunc_enabled' => $wp_cache_mfunc_enabled, + 'wp_supercache_304' => $wp_supercache_304, + 'wp_cache_no_cache_for_get' => $wp_cache_no_cache_for_get, + 'wp_cache_mutex_disabled' => $wp_cache_mutex_disabled, + 'cache_jetpack' => $cache_jetpack, + 'cache_domain_mapping' => $cache_domain_mapping, + ); + } + + public function add_wp_super_cache_constants_whitelist( $list ) { + return array_merge( $list, self::$wp_super_cache_constants ); + } + + public function add_wp_super_cache_callable_whitelist( $list ) { + return array_merge( $list, self::$wp_super_cache_callables ); + } +} 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..995e5ec3 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-module.php @@ -0,0 +1,174 @@ +<?php + +/** + * Basic methods implemented by Jetpack Sync extensions + */ +abstract class Jetpack_Sync_Module { + const ARRAY_CHUNK_SIZE = 10; + + abstract public function name(); + + public function get_object_by_id( $object_type, $id ) { + return false; + } + + // override these to set up listeners and set/reset data/defaults + public function init_listeners( $callable ) { + } + + public function init_full_sync_listeners( $callable ) { + } + + public function init_before_send() { + } + + public function set_defaults() { + } + + public function reset_data() { + } + + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { + // in subclasses, return the number of actions enqueued, and next module state (true == done) + return array( 0, true ); + } + + public function estimate_full_sync_actions( $config ) { + // in subclasses, return the number of items yet to be enqueued + return 0; + } + + public function get_full_sync_actions() { + return array(); + } + + protected function count_actions( $action_names, $actions_to_count ) { + return count( array_intersect( $action_names, $actions_to_count ) ); + } + + protected function get_check_sum( $values ) { + return crc32( wp_json_encode( jetpack_json_wrap( $values ) ) ); + } + + protected function still_valid_checksum( $sums_to_check, $name, $new_sum ) { + if ( isset( $sums_to_check[ $name ] ) && $sums_to_check[ $name ] === $new_sum ) { + return true; + } + + return false; + } + + protected function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql, $max_items_to_enqueue, $state ) { + global $wpdb; + + if ( ! $where_sql ) { + $where_sql = '1 = 1'; + } + + $items_per_page = 1000; + $page = 1; + $chunk_count = 0; + $previous_interval_end = $state ? $state : '~0'; + $listener = Jetpack_Sync_Listener::get_instance(); + + // count down from max_id to min_id so we get newest posts/comments/etc first + while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} < {$previous_interval_end} ORDER BY {$id_field} DESC LIMIT {$items_per_page}" ) ) { + // Request posts in groups of N for efficiency + $chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE ); + + // if we hit our row limit, process and return + if ( $chunk_count + count( $chunked_ids ) >= $max_items_to_enqueue ) { + $remaining_items_count = $max_items_to_enqueue - $chunk_count; + $remaining_items = array_slice( $chunked_ids, 0, $remaining_items_count ); + $remaining_items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $remaining_items, $previous_interval_end ); + $listener->bulk_enqueue_full_sync_actions( $action_name, $remaining_items_with_previous_interval_end ); + + $last_chunk = end( $remaining_items ); + return array( $remaining_items_count + $chunk_count, end( $last_chunk ) ); + } + $chunked_ids_with_previous_end = $this->get_chunks_with_preceding_end( $chunked_ids, $previous_interval_end ); + + $listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids_with_previous_end ); + + $chunk_count += count( $chunked_ids ); + $page += 1; + // $ids are ordered in descending order + $previous_interval_end = end( $ids ); + } + + return array( $chunk_count, true ); + } + + private function get_chunks_with_preceding_end( $chunks, $previous_interval_end ) { + foreach( $chunks as $chunk ) { + $chunks_with_ends[] = array( + 'ids' => $chunk, + 'previous_end' => $previous_interval_end + ); + // Chunks are ordered in descending order + $previous_interval_end = end( $chunk ); + } + return $chunks_with_ends; + } + + protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { + global $wpdb; + $table = _get_meta_table( $meta_type ); + $id = $meta_type . '_id'; + if ( ! $table ) { + return array(); + } + + $private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'"; + + return array_map( + array( $this, 'unserialize_meta' ), + $wpdb->get_results( + "SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )' . + " AND meta_key IN ( $private_meta_whitelist_sql ) ", + OBJECT + ) + ); + } + + public function init_listeners_for_meta_type( $meta_type, $callable ) { + add_action( "added_{$meta_type}_meta", $callable, 10, 4 ); + add_action( "updated_{$meta_type}_meta", $callable, 10, 4 ); + add_action( "deleted_{$meta_type}_meta", $callable, 10, 4 ); + } + + public function init_meta_whitelist_handler( $meta_type, $whitelist_handler ) { + add_filter( "jetpack_sync_before_enqueue_added_{$meta_type}_meta", $whitelist_handler ); + add_filter( "jetpack_sync_before_enqueue_updated_{$meta_type}_meta", $whitelist_handler ); + add_filter( "jetpack_sync_before_enqueue_deleted_{$meta_type}_meta", $whitelist_handler ); + } + + protected function get_term_relationships( $ids ) { + global $wpdb; + + return $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )', OBJECT ); + } + + public function unserialize_meta( $meta ) { + $meta->meta_value = maybe_unserialize( $meta->meta_value ); + return $meta; + } + + public function get_objects_by_id( $object_type, $ids ) { + if ( empty( $ids ) || empty( $object_type ) ) { + return array(); + } + + $objects = array(); + foreach ( (array) $ids as $id ) { + $object = $this->get_object_by_id( $object_type, $id ); + + // Only add object if we have the object. + if ( $object ) { + $objects[ $id ] = $object; + } + } + + return $objects; + } +} 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..2450bae9 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-modules.php @@ -0,0 +1,104 @@ +<?php + +/** + * simple wrapper that allows enumerating cached static instances + * of sync modules + */ + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-posts.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-import.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-comments.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-constants.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-callables.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-options.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-network-options.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-updates.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-users.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-themes.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-menus.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-attachments.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-meta.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-terms.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-plugins.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-full-sync.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-stats.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-module-protect.php'; + +class Jetpack_Sync_Modules { + + private static $default_sync_modules = array( + 'Jetpack_Sync_Module_Constants', + 'Jetpack_Sync_Module_Callables', + 'Jetpack_Sync_Module_Options', + 'Jetpack_Sync_Module_Network_Options', + 'Jetpack_Sync_Module_Terms', + 'Jetpack_Sync_Module_Themes', + 'Jetpack_Sync_Module_Menus', + 'Jetpack_Sync_Module_Users', + 'Jetpack_Sync_Module_Posts', + 'Jetpack_Sync_Module_Import', + 'Jetpack_Sync_Module_Protect', + 'Jetpack_Sync_Module_Comments', + 'Jetpack_Sync_Module_Updates', + 'Jetpack_Sync_Module_Attachments', + 'Jetpack_Sync_Module_Meta', + 'Jetpack_Sync_Module_Plugins', + 'Jetpack_Sync_Module_Full_Sync', + 'Jetpack_Sync_Module_Stats', + ); + + private static $initialized_modules = null; + + public static function get_modules() { + if ( null === self::$initialized_modules ) { + self::$initialized_modules = self::initialize_modules(); + } + + return self::$initialized_modules; + } + + public static function set_defaults() { + foreach ( self::get_modules() as $module ) { + $module->set_defaults(); + } + } + + public static function get_module( $module_name ) { + foreach ( self::get_modules() as $module ) { + if ( $module->name() === $module_name ) { + return $module; + } + } + + return false; + } + + static function initialize_modules() { + /** + * Filters the list of class names of sync modules. + * If you add to this list, make sure any classes implement the + * Jetpack_Sync_Module interface. + * + * @since 4.2.0 + */ + $modules = apply_filters( 'jetpack_sync_modules', self::$default_sync_modules ); + + $modules = array_map( array( 'Jetpack_Sync_Modules', 'load_module' ), $modules ); + + return array_map( array( 'Jetpack_Sync_Modules', 'set_module_defaults' ), $modules ); + } + + static function load_module( $module_name ) { + return new $module_name(); + } + + static function set_module_defaults( $module ) { + $module->set_defaults(); + if ( method_exists( $module, 'set_late_default' ) ) { + add_action( 'init', array( $module, 'set_late_default' ), 90 ); + } + return $module; + } + +} 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..ae40a597 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-queue.php @@ -0,0 +1,478 @@ +<?php + +/** + * A buffer of items from the queue that can be checked out + */ +class Jetpack_Sync_Queue_Buffer { + public $id; + public $items_with_ids; + + public function __construct( $id, $items_with_ids ) { + $this->id = $id; + $this->items_with_ids = $items_with_ids; + } + + public function get_items() { + return array_combine( $this->get_item_ids(), $this->get_item_values() ); + } + + public function get_item_values() { + return Jetpack_Sync_Utils::get_item_values( $this->items_with_ids ); + } + + public function get_item_ids() { + return Jetpack_Sync_Utils::get_item_ids( $this->items_with_ids ); + } +} + +/** + * A persistent queue that can be flushed in increments of N items, + * and which blocks reads until checked-out buffers are checked in or + * closed. This uses raw SQL for two reasons: speed, and not triggering + * tons of added_option callbacks. + */ +class Jetpack_Sync_Queue { + public $id; + private $row_iterator; + + function __construct( $id ) { + $this->id = str_replace( '-', '_', $id ); // necessary to ensure we don't have ID collisions in the SQL + $this->row_iterator = 0; + $this->random_int = mt_rand( 1, 1000000 ); + } + + function add( $item ) { + global $wpdb; + $added = false; + // this basically tries to add the option until enough time has elapsed that + // it has a unique (microtime-based) option key + while ( ! $added ) { + $rows_added = $wpdb->query( + $wpdb->prepare( + "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES (%s, %s,%s)", + $this->get_next_data_row_option_name(), + serialize( $item ), + 'no' + ) + ); + $added = ( 0 !== $rows_added ); + } + } + + // Attempts to insert all the items in a single SQL query. May be subject to query size limits! + function add_all( $items ) { + global $wpdb; + $base_option_name = $this->get_next_data_row_option_name(); + + $query = "INSERT INTO $wpdb->options (option_name, option_value, autoload) VALUES "; + + $rows = array(); + + for ( $i = 0; $i < count( $items ); $i += 1 ) { + $option_name = esc_sql( $base_option_name . '-' . $i ); + $option_value = esc_sql( serialize( $items[ $i ] ) ); + $rows[] = "('$option_name', '$option_value', 'no')"; + } + + $rows_added = $wpdb->query( $query . join( ',', $rows ) ); + + if ( count( $items ) === $rows_added ) { + return new WP_Error( 'row_count_mismatch', "The number of rows inserted didn't match the size of the input array" ); + } + } + + // Peek at the front-most item on the queue without checking it out + function peek( $count = 1 ) { + $items = $this->fetch_items( $count ); + if ( $items ) { + return Jetpack_Sync_Utils::get_item_values( $items ); + } + + return array(); + } + + // lag is the difference in time between the age of the oldest item + // (aka first or frontmost item) and the current time + function lag( $now = null ) { + global $wpdb; + + $first_item_name = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT 1", + "jpsq_{$this->id}-%" + ) + ); + + if ( ! $first_item_name ) { + return 0; + } + + if ( null === $now ) { + $now = microtime( true ); + } + + // break apart the item name to get the timestamp + $matches = null; + if ( preg_match( '/^jpsq_' . $this->id . '-(\d+\.\d+)-/', $first_item_name, $matches ) ) { + return $now - floatval( $matches[1] ); + } else { + return 0; + } + } + + function reset() { + global $wpdb; + $this->delete_checkout_id(); + $wpdb->query( + $wpdb->prepare( + "DELETE FROM $wpdb->options WHERE option_name LIKE %s", + "jpsq_{$this->id}-%" + ) + ); + } + + function size() { + global $wpdb; + + return (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT count(*) FROM $wpdb->options WHERE option_name LIKE %s", + "jpsq_{$this->id}-%" + ) + ); + } + + // we use this peculiar implementation because it's much faster than count(*) + function has_any_items() { + global $wpdb; + $value = $wpdb->get_var( + $wpdb->prepare( + "SELECT exists( SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s )", + "jpsq_{$this->id}-%" + ) + ); + + return ( $value === '1' ); + } + + function checkout( $buffer_size ) { + if ( $this->get_checkout_id() ) { + return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + $buffer_id = uniqid(); + + $result = $this->set_checkout_id( $buffer_id ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + $items = $this->fetch_items( $buffer_size ); + + if ( count( $items ) === 0 ) { + return false; + } + + $buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, array_slice( $items, 0, $buffer_size ) ); + + return $buffer; + } + + // this checks out rows until it either empties the queue or hits a certain memory limit + // it loads the sizes from the DB first so that it doesn't accidentally + // load more data into memory than it needs to. + // The only way it will load more items than $max_size is if a single queue item + // exceeds the memory limit, but in that case it will send that item by itself. + function checkout_with_memory_limit( $max_memory, $max_buffer_size = 500 ) { + if ( $this->get_checkout_id() ) { + return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + $buffer_id = uniqid(); + + $result = $this->set_checkout_id( $buffer_id ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + // get the map of buffer_id -> memory_size + global $wpdb; + + $items_with_size = $wpdb->get_results( + $wpdb->prepare( + "SELECT option_name AS id, LENGTH(option_value) AS value_size FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d", + "jpsq_{$this->id}-%", + $max_buffer_size + ), + OBJECT + ); + + if ( count( $items_with_size ) === 0 ) { + return false; + } + + $total_memory = 0; + + $min_item_id = $max_item_id = $items_with_size[0]->id; + + foreach ( $items_with_size as $id => $item_with_size ) { + $total_memory += $item_with_size->value_size; + + // if this is the first item and it exceeds memory, allow loop to continue + // we will exit on the next iteration instead + if ( $total_memory > $max_memory && $id > 0 ) { + break; + } + + $max_item_id = $item_with_size->id; + } + + $query = $wpdb->prepare( + "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name >= %s and option_name <= %s ORDER BY option_name ASC", + $min_item_id, + $max_item_id + ); + + $items = $wpdb->get_results( $query, OBJECT ); + foreach ( $items as $item ) { + $item->value = maybe_unserialize( $item->value ); + } + + if ( count( $items ) === 0 ) { + $this->delete_checkout_id(); + + return false; + } + + $buffer = new Jetpack_Sync_Queue_Buffer( $buffer_id, $items ); + + return $buffer; + } + + function checkin( $buffer ) { + $is_valid = $this->validate_checkout( $buffer ); + + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + + $this->delete_checkout_id(); + + return true; + } + + function close( $buffer, $ids_to_remove = null ) { + $is_valid = $this->validate_checkout( $buffer ); + + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + + $this->delete_checkout_id(); + + // by default clear all items in the buffer + if ( is_null( $ids_to_remove ) ) { + $ids_to_remove = $buffer->get_item_ids(); + } + + global $wpdb; + + if ( count( $ids_to_remove ) > 0 ) { + $sql = "DELETE FROM $wpdb->options WHERE option_name IN (" . implode( ', ', array_fill( 0, count( $ids_to_remove ), '%s' ) ) . ')'; + $query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $ids_to_remove ) ); + $wpdb->query( $query ); + } + + return true; + } + + function flush_all() { + $items = Jetpack_Sync_Utils::get_item_values( $this->fetch_items() ); + $this->reset(); + + return $items; + } + + function get_all() { + return $this->fetch_items(); + } + + // use with caution, this could allow multiple processes to delete + // and send from the queue at the same time + function force_checkin() { + $this->delete_checkout_id(); + } + + // used to lock checkouts from the queue. + // tries to wait up to $timeout seconds for the queue to be empty + function lock( $timeout = 30 ) { + $tries = 0; + + while ( $this->has_any_items() && $tries < $timeout ) { + sleep( 1 ); + $tries += 1; + } + + if ( $tries === 30 ) { + return new WP_Error( 'lock_timeout', 'Timeout waiting for sync queue to empty' ); + } + + if ( $this->get_checkout_id() ) { + return new WP_Error( 'unclosed_buffer', 'There is an unclosed buffer' ); + } + + // hopefully this means we can acquire a checkout? + $result = $this->set_checkout_id( 'lock' ); + + if ( ! $result || is_wp_error( $result ) ) { + return $result; + } + + return true; + } + + function unlock() { + return $this->delete_checkout_id(); + } + + /** + * 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. + * + * @return string + */ + protected function generate_option_name_timestamp() { + return sprintf( '%.6f', microtime( true ) ); + } + + private function get_checkout_id() { + global $wpdb; + $checkout_value = $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s", + $this->get_lock_option_name() + ) + ); + + if ( $checkout_value ) { + list( $checkout_id, $timestamp ) = explode( ':', $checkout_value ); + if ( intval( $timestamp ) > time() ) { + return $checkout_id; + } + } + + return false; + } + + private function set_checkout_id( $checkout_id ) { + global $wpdb; + + $expires = time() + Jetpack_Sync_Defaults::$default_sync_queue_lock_timeout; + $updated_num = $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", + "$checkout_id:$expires", + $this->get_lock_option_name() + ) + ); + + if ( ! $updated_num ) { + $updated_num = $wpdb->query( + $wpdb->prepare( + "INSERT INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )", + $this->get_lock_option_name(), + "$checkout_id:$expires" + ) + ); + } + + return $updated_num; + } + + private function delete_checkout_id() { + global $wpdb; + // rather than delete, which causes fragmentation, we update in place + return $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s", + '0:0', + $this->get_lock_option_name() + ) + ); + + } + + private function get_lock_option_name() { + return "jpsq_{$this->id}_checkout"; + } + + private function get_next_data_row_option_name() { + $timestamp = $this->generate_option_name_timestamp(); + + // row iterator is used to avoid collisions where we're writing data waaay fast in a single process + if ( $this->row_iterator === PHP_INT_MAX ) { + $this->row_iterator = 0; + } else { + $this->row_iterator += 1; + } + + return 'jpsq_' . $this->id . '-' . $timestamp . '-' . $this->random_int . '-' . $this->row_iterator; + } + + private function fetch_items( $limit = null ) { + global $wpdb; + + if ( $limit ) { + $query_sql = $wpdb->prepare( "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC LIMIT %d", "jpsq_{$this->id}-%", $limit ); + } else { + $query_sql = $wpdb->prepare( "SELECT option_name AS id, option_value AS value FROM $wpdb->options WHERE option_name LIKE %s ORDER BY option_name ASC", "jpsq_{$this->id}-%" ); + } + + $items = $wpdb->get_results( $query_sql, OBJECT ); + foreach ( $items as $item ) { + $item->value = maybe_unserialize( $item->value ); + } + + return $items; + } + + private function validate_checkout( $buffer ) { + if ( ! $buffer instanceof Jetpack_Sync_Queue_Buffer ) { + return new WP_Error( 'not_a_buffer', 'You must checkin an instance of Jetpack_Sync_Queue_Buffer' ); + } + + $checkout_id = $this->get_checkout_id(); + + if ( ! $checkout_id ) { + return new WP_Error( 'buffer_not_checked_out', 'There are no checked out buffers' ); + } + + if ( $checkout_id != $buffer->id ) { + return new WP_Error( 'buffer_mismatch', 'The buffer you checked in was not checked out' ); + } + + return true; + } +} + +class Jetpack_Sync_Utils { + + static function get_item_values( $items ) { + return array_map( array( __CLASS__, 'get_item_value' ), $items ); + } + + static function get_item_ids( $items ) { + return array_map( array( __CLASS__, 'get_item_id' ), $items ); + } + + private static function get_item_value( $item ) { + return $item->value; + } + + private static 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..21f91481 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-sender.php @@ -0,0 +1,416 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-queue.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-array-codec.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-simple-codec.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-modules.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-settings.php'; + +/** + * This class grabs pending actions from the queue and sends them + */ +class Jetpack_Sync_Sender { + + const NEXT_SYNC_TIME_OPTION_NAME = 'jetpack_next_sync_time'; + const WPCOM_ERROR_SYNC_DELAY = 60; + const QUEUE_LOCKED_SYNC_DELAY = 10; + + private $dequeue_max_bytes; + private $upload_max_bytes; + private $upload_max_rows; + private $max_dequeue_time; + private $sync_wait_time; + private $sync_wait_threshold; + private $enqueue_wait_time; + private $sync_queue; + private $full_sync_queue; + private $codec; + private $old_user; + + // singleton functions + private static $instance; + + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + // this is necessary because you can't use "new" when you declare instance properties >:( + protected function __construct() { + $this->set_defaults(); + $this->init(); + } + + private function init() { + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_set_user_from_token' ), 1 ); + add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_clear_user_from_token' ), 20 ); + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module->init_before_send(); + } + } + + public function maybe_set_user_from_token() { + $jetpack = Jetpack::init(); + $verified_user = $jetpack->verify_xml_rpc_signature(); + if ( Jetpack_Constants::is_true( 'XMLRPC_REQUEST' ) && + ! is_wp_error( $verified_user ) + && $verified_user + ) { + $old_user = wp_get_current_user(); + $this->old_user = isset( $old_user->ID ) ? $old_user->ID : 0; + wp_set_current_user( $verified_user['user_id'] ); + } + } + + public function maybe_clear_user_from_token() { + if ( isset( $this->old_user ) ) { + wp_set_current_user( $this->old_user ); + } + } + + public function get_next_sync_time( $queue_name ) { + return (float) get_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, 0 ); + } + + public function set_next_sync_time( $time, $queue_name ) { + return update_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name, $time, true ); + } + + public function do_full_sync() { + if ( ! Jetpack_Sync_Modules::get_module( 'full-sync' ) ) { + return; + } + $this->continue_full_sync_enqueue(); + return $this->do_sync_and_set_delays( $this->full_sync_queue ); + } + + private function continue_full_sync_enqueue() { + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return false; + } + + if ( $this->get_next_sync_time( 'full-sync-enqueue' ) > microtime( true ) ) { + return false; + } + + Jetpack_Sync_Modules::get_module( 'full-sync' )->continue_enqueuing(); + + $this->set_next_sync_time( time() + $this->get_enqueue_wait_time(), 'full-sync-enqueue' ); + } + + public function do_sync() { + return $this->do_sync_and_set_delays( $this->sync_queue ); + } + + public function do_sync_and_set_delays( $queue ) { + // don't sync if importing + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return new WP_Error( 'is_importing' ); + } + + // don't sync if we are throttled + if ( $this->get_next_sync_time( $queue->id ) > microtime( true ) ) { + return new WP_Error( 'sync_throttled' ); + } + + $start_time = microtime( true ); + + Jetpack_Sync_Settings::set_is_syncing( true ); + + $sync_result = $this->do_sync_for_queue( $queue ); + + Jetpack_Sync_Settings::set_is_syncing( false ); + + $exceeded_sync_wait_threshold = ( microtime( true ) - $start_time ) > (float) $this->get_sync_wait_threshold(); + + if ( is_wp_error( $sync_result ) ) { + if ( 'unclosed_buffer' === $sync_result->get_error_code() ) { + $this->set_next_sync_time( time() + self::QUEUE_LOCKED_SYNC_DELAY, $queue->id ); + } + if ( 'wpcom_error' === $sync_result->get_error_code() ) { + $this->set_next_sync_time( time() + self::WPCOM_ERROR_SYNC_DELAY, $queue->id ); + } + } elseif ( $exceeded_sync_wait_threshold ) { + // if we actually sent data and it took a while, wait before sending again + $this->set_next_sync_time( time() + $this->get_sync_wait_time(), $queue->id ); + } + + return $sync_result; + } + + public function get_items_to_send( $buffer, $encode = true ) { + // track how long we've been processing so we can avoid request timeouts + $start_time = microtime( true ); + $upload_size = 0; + $items_to_send = array(); + $items = $buffer->get_items(); + // set up current screen to avoid errors rendering content + require_once ABSPATH . 'wp-admin/includes/class-wp-screen.php'; + require_once ABSPATH . 'wp-admin/includes/screen.php'; + set_current_screen( 'sync' ); + $skipped_items_ids = array(); + // we estimate the total encoded size as we go by encoding each item individually + // this is expensive, but the only way to really know :/ + foreach ( $items as $key => $item ) { + // Suspending cache addition help prevent overloading in memory cache of large sites. + wp_suspend_cache_addition( true ); + /** + * Modify the data within an action before it is serialized and sent to the server + * For example, during full sync this expands Post ID's into full Post objects, + * so that we don't have to serialize the whole object into the queue. + * + * @since 4.2.0 + * + * @param array The action parameters + * @param int The ID of the user who triggered the action + */ + $item[1] = apply_filters( 'jetpack_sync_before_send_' . $item[0], $item[1], $item[2] ); + wp_suspend_cache_addition( false ); + if ( $item[1] === false ) { + $skipped_items_ids[] = $key; + continue; + } + $encoded_item = $encode ? $this->codec->encode( $item ) : $item; + $upload_size += strlen( $encoded_item ); + if ( $upload_size > $this->upload_max_bytes && count( $items_to_send ) > 0 ) { + break; + } + $items_to_send[ $key ] = $encoded_item; + if ( microtime( true ) - $start_time > $this->max_dequeue_time ) { + break; + } + } + + return array( $items_to_send, $skipped_items_ids, $items, microtime( true ) - $start_time ); + } + + private function fastcgi_finish_request() { + if ( function_exists( 'fastcgi_finish_request' ) && version_compare( phpversion(), '7.0.16', '>=' ) ) { + fastcgi_finish_request(); + } + } + + public function do_sync_for_queue( $queue ) { + do_action( 'jetpack_sync_before_send_queue_' . $queue->id ); + if ( $queue->size() === 0 ) { + return new WP_Error( 'empty_queue_' . $queue->id ); + } + // now that we're sure we are about to sync, try to + // ignore user abort so we can avoid getting into a + // bad state + if ( function_exists( 'ignore_user_abort' ) ) { + ignore_user_abort( true ); + } + + /* Don't make the request block till we finish, if possible. */ + if ( Jetpack_Constants::is_true( 'REST_REQUEST' ) || Jetpack_Constants::is_true('XMLRPC_REQUEST' ) ) { + $this->fastcgi_finish_request(); + } + + $checkout_start_time = microtime( true ); + + $buffer = $queue->checkout_with_memory_limit( $this->dequeue_max_bytes, $this->upload_max_rows ); + + if ( ! $buffer ) { + // buffer has no items + return new WP_Error( 'empty_buffer' ); + } + + if ( is_wp_error( $buffer ) ) { + return $buffer; + } + + $checkout_duration = microtime( true ) - $checkout_start_time; + + list( $items_to_send, $skipped_items_ids, $items, $preprocess_duration ) = $this->get_items_to_send( $buffer, true ); + if ( ! empty( $items_to_send ) ) { + /** + * Fires when data is ready to send to the server. + * Return false or WP_Error to abort the sync (e.g. if there's an error) + * The items will be automatically re-sent later + * + * @since 4.2.0 + * + * @param array $data The action buffer + * @param string $codec The codec name used to encode the data + * @param double $time The current time + * @param string $queue The queue used to send ('sync' or 'full_sync') + */ + Jetpack_Sync_Settings::set_is_sending( true ); + $processed_item_ids = apply_filters( 'jetpack_sync_send_data', $items_to_send, $this->codec->name(), microtime( true ), $queue->id, $checkout_duration, $preprocess_duration ); + Jetpack_Sync_Settings::set_is_sending( false ); + } else { + $processed_item_ids = $skipped_items_ids; + $skipped_items_ids = array(); + } + + if ( ! $processed_item_ids || is_wp_error( $processed_item_ids ) ) { + $checked_in_item_ids = $queue->checkin( $buffer ); + if ( is_wp_error( $checked_in_item_ids ) ) { + error_log( 'Error checking in buffer: ' . $checked_in_item_ids->get_error_message() ); + $queue->force_checkin(); + } + if ( is_wp_error( $processed_item_ids ) ) { + return new WP_Error( 'wpcom_error', $processed_item_ids->get_error_code() ); + } + // returning a WP_Error('wpcom_error') is a sign to the caller that we should wait a while + // before syncing again + return new WP_Error( 'wpcom_error', 'jetpack_sync_send_data_false' ); + } else { + // detect if the last item ID was an error + $had_wp_error = is_wp_error( end( $processed_item_ids ) ); + if ( $had_wp_error ) { + $wp_error = array_pop( $processed_item_ids ); + } + // also checkin any items that were skipped + if ( count( $skipped_items_ids ) > 0 ) { + $processed_item_ids = array_merge( $processed_item_ids, $skipped_items_ids ); + } + $processed_items = array_intersect_key( $items, array_flip( $processed_item_ids ) ); + /** + * Allows us to keep track of all the actions that have been sent. + * Allows us to calculate the progress of specific actions. + * + * @since 4.2.0 + * + * @param array $processed_actions The actions that we send successfully. + */ + do_action( 'jetpack_sync_processed_actions', $processed_items ); + $queue->close( $buffer, $processed_item_ids ); + // returning a WP_Error is a sign to the caller that we should wait a while + // before syncing again + if ( $had_wp_error ) { + return new WP_Error( 'wpcom_error', $wp_error->get_error_code() ); + } + } + return true; + } + + function get_sync_queue() { + return $this->sync_queue; + } + + function get_full_sync_queue() { + return $this->full_sync_queue; + } + + function get_codec() { + return $this->codec; + } + function set_codec() { + if ( function_exists( 'gzinflate' ) ) { + $this->codec = new Jetpack_Sync_JSON_Deflate_Array_Codec(); + } else { + $this->codec = new Jetpack_Sync_Simple_Codec(); + } + } + + function send_checksum() { + require_once 'class.jetpack-sync-wp-replicastore.php'; + $store = new Jetpack_Sync_WP_Replicastore(); + do_action( 'jetpack_sync_checksum', $store->checksum_all() ); + } + + function reset_sync_queue() { + $this->sync_queue->reset(); + } + + function reset_full_sync_queue() { + $this->full_sync_queue->reset(); + } + + function set_dequeue_max_bytes( $size ) { + $this->dequeue_max_bytes = $size; + } + + // in bytes + function set_upload_max_bytes( $max_bytes ) { + $this->upload_max_bytes = $max_bytes; + } + + // in rows + function set_upload_max_rows( $max_rows ) { + $this->upload_max_rows = $max_rows; + } + + // in seconds + function set_sync_wait_time( $seconds ) { + $this->sync_wait_time = $seconds; + } + + function get_sync_wait_time() { + return $this->sync_wait_time; + } + + function set_enqueue_wait_time( $seconds ) { + $this->enqueue_wait_time = $seconds; + } + + function get_enqueue_wait_time() { + return $this->enqueue_wait_time; + } + + // in seconds + function set_sync_wait_threshold( $seconds ) { + $this->sync_wait_threshold = $seconds; + } + + function get_sync_wait_threshold() { + return $this->sync_wait_threshold; + } + + // in seconds + function set_max_dequeue_time( $seconds ) { + $this->max_dequeue_time = $seconds; + } + + + + function set_defaults() { + $this->sync_queue = new Jetpack_Sync_Queue( 'sync' ); + $this->full_sync_queue = new Jetpack_Sync_Queue( 'full_sync' ); + $this->set_codec(); + + // saved settings + Jetpack_Sync_Settings::set_importing( null ); + $settings = Jetpack_Sync_Settings::get_settings(); + $this->set_dequeue_max_bytes( $settings['dequeue_max_bytes'] ); + $this->set_upload_max_bytes( $settings['upload_max_bytes'] ); + $this->set_upload_max_rows( $settings['upload_max_rows'] ); + $this->set_sync_wait_time( $settings['sync_wait_time'] ); + $this->set_enqueue_wait_time( $settings['enqueue_wait_time'] ); + $this->set_sync_wait_threshold( $settings['sync_wait_threshold'] ); + $this->set_max_dequeue_time( Jetpack_Sync_Defaults::get_max_sync_execution_time() ); + } + + function reset_data() { + $this->reset_sync_queue(); + $this->reset_full_sync_queue(); + + foreach ( Jetpack_Sync_Modules::get_modules() as $module ) { + $module->reset_data(); + } + + foreach ( array( 'sync', 'full_sync', 'full-sync-enqueue' ) as $queue_name ) { + delete_option( self::NEXT_SYNC_TIME_OPTION_NAME . '_' . $queue_name ); + } + + Jetpack_Sync_Settings::reset_data(); + } + + function uninstall() { + // Lets delete all the other fun stuff like transient and option and the sync queue + $this->reset_data(); + + // delete the full sync status + delete_option( 'jetpack_full_sync_status' ); + + // clear the sync cron. + wp_clear_scheduled_hook( 'jetpack_sync_cron' ); + wp_clear_scheduled_hook( 'jetpack_sync_full_cron' ); + } +} 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..dbd9172a --- /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-array-codec.php'; + +/** + * Simple version of a Jetpack Sync Server - just receives arrays of events and + * issues them locally with the 'jetpack_sync_remote_action' action. + */ +class Jetpack_Sync_Server { + private $codec; + const MAX_TIME_PER_REQUEST_IN_SECONDS = 15; + const BLOG_LOCK_TRANSIENT_PREFIX = 'jp_sync_req_lock_'; + const BLOG_LOCK_TRANSIENT_EXPIRY = 60; // seconds + + // this is necessary because you can't use "new" when you declare instance properties >:( + function __construct() { + $this->codec = new Jetpack_Sync_JSON_Deflate_Array_Codec(); + } + + function set_codec( iJetpack_Sync_Codec $codec ) { + $this->codec = $codec; + } + + function attempt_request_lock( $blog_id, $expiry = self::BLOG_LOCK_TRANSIENT_EXPIRY ) { + $transient_name = $this->get_concurrent_request_transient_name( $blog_id ); + $locked_time = get_site_transient( $transient_name ); + if ( $locked_time ) { + return false; + } + set_site_transient( $transient_name, microtime( true ), $expiry ); + + return true; + } + + private function get_concurrent_request_transient_name( $blog_id ) { + return self::BLOG_LOCK_TRANSIENT_PREFIX . $blog_id; + } + + function remove_request_lock( $blog_id ) { + delete_site_transient( $this->get_concurrent_request_transient_name( $blog_id ) ); + } + + function receive( $data, $token = null, $sent_timestamp = null, $queue_id = null ) { + $start_time = microtime( true ); + if ( ! is_array( $data ) ) { + return new WP_Error( 'action_decoder_error', 'Events must be an array' ); + } + + if ( $token && ! $this->attempt_request_lock( $token->blog_id ) ) { + /** + * Fires when the server receives two concurrent requests from the same blog + * + * @since 4.2.0 + * + * @param token The token object of the misbehaving site + */ + do_action( 'jetpack_sync_multi_request_fail', $token ); + + return new WP_Error( 'concurrent_request_error', 'There is another request running for the same blog ID' ); + } + + $events = wp_unslash( array_map( array( $this->codec, 'decode' ), $data ) ); + $events_processed = array(); + + /** + * Fires when an array of actions are received from a remote Jetpack site + * + * @since 4.2.0 + * + * @param array Array of actions received from the remote site + */ + do_action( 'jetpack_sync_remote_actions', $events, $token ); + + foreach ( $events as $key => $event ) { + list( $action_name, $args, $user_id, $timestamp, $silent ) = $event; + + /** + * Fires when an action is received from a remote Jetpack site + * + * @since 4.2.0 + * + * @param string $action_name The name of the action executed on the remote site + * @param array $args The arguments passed to the action + * @param int $user_id The external_user_id who did the action + * @param bool $silent Whether the item was created via import + * @param double $timestamp Timestamp (in seconds) when the action occurred + * @param double $sent_timestamp Timestamp (in seconds) when the action was transmitted + * @param string $queue_id ID of the queue from which the event was sent (sync or full_sync) + * @param array $token The auth token used to invoke the API + */ + do_action( 'jetpack_sync_remote_action', $action_name, $args, $user_id, $silent, $timestamp, $sent_timestamp, $queue_id, $token ); + + $events_processed[] = $key; + + if ( microtime( true ) - $start_time > self::MAX_TIME_PER_REQUEST_IN_SECONDS ) { + break; + } + } + + if ( $token ) { + $this->remove_request_lock( $token->blog_id ); + } + + return $events_processed; + } +} 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..e77588a8 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-settings.php @@ -0,0 +1,215 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php'; + +class Jetpack_Sync_Settings { + const SETTINGS_OPTION_PREFIX = 'jetpack_sync_settings_'; + + static $valid_settings = array( + 'dequeue_max_bytes' => true, + 'upload_max_bytes' => true, + 'upload_max_rows' => true, + 'sync_wait_time' => true, + 'sync_wait_threshold' => true, + 'enqueue_wait_time' => true, + 'max_queue_size' => true, + 'max_queue_lag' => true, + 'queue_max_writes_sec' => true, + 'post_types_blacklist' => true, + 'disable' => true, + 'network_disable' => true, + 'render_filtered_content' => true, + 'post_meta_whitelist' => true, + 'comment_meta_whitelist' => true, + 'max_enqueue_full_sync' => true, + 'max_queue_size_full_sync' => true, + 'sync_via_cron' => true, + 'cron_sync_time_limit' => true, + 'known_importers' => true, + ); + + static $is_importing; + static $is_doing_cron; + static $is_syncing; + static $is_sending; + + static $settings_cache = array(); // some settings can be expensive to compute - let's cache them + + static function get_settings() { + $settings = array(); + foreach ( array_keys( self::$valid_settings ) as $setting ) { + $settings[ $setting ] = self::get_setting( $setting ); + } + + return $settings; + } + + // Fetches the setting. It saves it if the setting doesn't exist, so that it gets + // autoloaded on page load rather than re-queried every time. + static function get_setting( $setting ) { + if ( ! isset( self::$valid_settings[ $setting ] ) ) { + return false; + } + + if ( isset( self::$settings_cache[ $setting ] ) ) { + return self::$settings_cache[ $setting ]; + } + + if ( self::is_network_setting( $setting ) ) { + if ( is_multisite() ) { + $value = get_site_option( self::SETTINGS_OPTION_PREFIX . $setting ); + } else { + // On single sites just return the default setting + $value = Jetpack_Sync_Defaults::get_default_setting( $setting ); + self::$settings_cache[ $setting ] = $value; + return $value; + } + } else { + $value = get_option( self::SETTINGS_OPTION_PREFIX . $setting ); + } + + if ( false === $value ) { // no default value is set. + $value = Jetpack_Sync_Defaults::get_default_setting( $setting ); + if ( self::is_network_setting( $setting ) ) { + update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value ); + } else { + // We set one so that it gets autoloaded + update_option( self::SETTINGS_OPTION_PREFIX . $setting, $value, true ); + } + } + + if ( is_numeric( $value ) ) { + $value = intval( $value ); + } + $default_array_value = null; + switch ( $setting ) { + case 'post_types_blacklist': + $default_array_value = Jetpack_Sync_Defaults::$blacklisted_post_types; + break; + case 'post_meta_whitelist': + $default_array_value = Jetpack_Sync_Defaults::get_post_meta_whitelist(); + break; + case 'comment_meta_whitelist': + $default_array_value = Jetpack_Sync_Defaults::get_comment_meta_whitelist(); + break; + case 'known_importers': + $default_array_value = Jetpack_Sync_Defaults::get_known_importers(); + break; + } + + if ( $default_array_value ) { + if ( is_array( $value ) ) { + $value = array_unique( array_merge( $value, $default_array_value ) ); + } else { + $value = $default_array_value; + } + } + + self::$settings_cache[ $setting ] = $value; + + return $value; + } + + static function update_settings( $new_settings ) { + $validated_settings = array_intersect_key( $new_settings, self::$valid_settings ); + foreach ( $validated_settings as $setting => $value ) { + + if ( self::is_network_setting( $setting ) ) { + if ( is_multisite() && is_main_site() ) { + update_site_option( self::SETTINGS_OPTION_PREFIX . $setting, $value ); + } + } else { + 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 || 'network_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(); + } + } + } + + static function is_network_setting( $setting ) { + return strpos( $setting, 'network_' ) === 0; + } + + // returns escapted SQL that can be injected into a WHERE clause + static function get_blacklisted_post_types_sql() { + return 'post_type NOT IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_types_blacklist' ) ) ) . '\')'; + } + + static function get_whitelisted_post_meta_sql() { + return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'post_meta_whitelist' ) ) ) . '\')'; + } + + static function get_whitelisted_comment_meta_sql() { + return 'meta_key IN (\'' . join( '\', \'', array_map( 'esc_sql', self::get_setting( 'comment_meta_whitelist' ) ) ) . '\')'; + } + + static function get_comments_filter_sql() { + return "comment_approved <> 'spam'"; + } + + static function reset_data() { + $valid_settings = self::$valid_settings; + self::$settings_cache = array(); + foreach ( $valid_settings as $option => $value ) { + delete_option( self::SETTINGS_OPTION_PREFIX . $option ); + } + self::set_importing( null ); + self::set_doing_cron( null ); + self::set_is_syncing( null ); + self::set_is_sending( null ); + } + + static function set_importing( $is_importing ) { + // set to NULL to revert to WP_IMPORTING, the standard behavior + 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 is_sync_enabled() { + return ! ( self::get_setting( 'disable' ) || self::get_setting( 'network_disable' ) ); + } + + static function set_doing_cron( $is_doing_cron ) { + // set to NULL to revert to WP_IMPORTING, the standard behavior + self::$is_doing_cron = $is_doing_cron; + } + + static function is_doing_cron() { + if ( ! is_null( self::$is_doing_cron ) ) { + return self::$is_doing_cron; + } + + return defined( 'DOING_CRON' ) && DOING_CRON; + } + + static function is_syncing() { + return (bool) self::$is_syncing || ( defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ); + } + + static function set_is_syncing( $is_syncing ) { + self::$is_syncing = $is_syncing; + } + + static function is_sending() { + return (bool) self::$is_sending; + } + + static function set_is_sending( $is_sending ) { + self::$is_sending = $is_sending; + } +} diff --git a/plugins/jetpack/sync/class.jetpack-sync-simple-codec.php b/plugins/jetpack/sync/class.jetpack-sync-simple-codec.php new file mode 100644 index 00000000..7bd9aaac --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-simple-codec.php @@ -0,0 +1,24 @@ +<?php + +require_once dirname( __FILE__ ) . '/class.jetpack-sync-json-deflate-array-codec.php'; + +/** + * An implementation of iJetpack_Sync_Codec that uses gzip's DEFLATE + * algorithm to compress objects serialized using json_encode + */ +class Jetpack_Sync_Simple_Codec extends Jetpack_Sync_JSON_Deflate_Array_Codec { + const CODEC_NAME = 'simple'; + + public function name() { + return self::CODEC_NAME; + } + + public function encode( $object ) { + return base64_encode( $this->json_serialize( $object ) ); + } + + public function decode( $input ) { + return $this->json_unserialize( base64_decode( $input ) ); + } + +} 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..de032e2d --- /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( self::$user_roles[ $user_id ] ) ) { + return self::$user_roles[ $user_id ]; + } + + $current_user_id = get_current_user_id(); + wp_set_current_user( $user_id ); + $role = Jetpack::translate_current_user_to_role(); + wp_set_current_user( $current_user_id ); + $user_roles[ $user_id ] = $role; + + return $role; + } + + static function get_signed_role( $user_id ) { + return Jetpack::sign_role( self::get_role( $user_id ), $user_id ); + } + + static function update_role_on_com( $user_id ) { + $signed_role = self::get_signed_role( $user_id ); + Jetpack::xmlrpc_async_call( 'jetpack.updateRole', $user_id, $signed_role ); + } + + static function maybe_demote_master_user( $user_id ) { + $master_user_id = Jetpack_Options::get_option( 'master_user' ); + $role = self::get_role( $user_id ); + if ( $user_id == $master_user_id && 'administrator' != $role ) { + $query = new WP_User_Query( + array( + 'fields' => array( 'id' ), + 'role' => 'administrator', + 'orderby' => 'id', + 'exclude' => array( $master_user_id ), + ) + ); + $new_master = false; + foreach ( $query->results as $result ) { + $found_user_id = absint( $result->id ); + if ( $found_user_id && Jetpack::is_user_connected( $found_user_id ) ) { + $new_master = $found_user_id; + break; + } + } + + if ( $new_master ) { + Jetpack_Options::update_option( 'master_user', $new_master ); + } + // else disconnect..? + } + } +} + +Jetpack_Sync_Users::init(); 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..99826bd0 --- /dev/null +++ b/plugins/jetpack/sync/class.jetpack-sync-wp-replicastore.php @@ -0,0 +1,820 @@ +<?php + +require_once dirname( __FILE__ ) . '/interface.jetpack-sync-replicastore.php'; +require_once dirname( __FILE__ ) . '/class.jetpack-sync-defaults.php'; + +/** + * An implementation of iJetpack_Sync_Replicastore which returns data stored in a WordPress.org DB. + * This is useful to compare values in the local WP DB to values in the synced replica store + */ +class Jetpack_Sync_WP_Replicastore implements iJetpack_Sync_Replicastore { + + + public function reset() { + global $wpdb; + + $wpdb->query( "DELETE FROM $wpdb->posts" ); + $wpdb->query( "DELETE FROM $wpdb->comments" ); + + // also need to delete terms from cache + $term_ids = $wpdb->get_col( "SELECT term_id FROM $wpdb->terms" ); + foreach ( $term_ids as $term_id ) { + wp_cache_delete( $term_id, 'terms' ); + } + + $wpdb->query( "DELETE FROM $wpdb->terms" ); + + $wpdb->query( "DELETE FROM $wpdb->term_taxonomy" ); + $wpdb->query( "DELETE FROM $wpdb->term_relationships" ); + + // callables and constants + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'jetpack_%'" ); + $wpdb->query( "DELETE FROM $wpdb->postmeta WHERE meta_key NOT LIKE '\_%'" ); + } + + function full_sync_start( $config ) { + $this->reset(); + } + + function full_sync_end( $checksum ) { + // noop right now + } + + public function post_count( $status = null, $min_id = null, $max_id = null ) { + global $wpdb; + + $where = ''; + + if ( $status ) { + $where = "post_status = '" . esc_sql( $status ) . "'"; + } else { + $where = '1=1'; + } + + if ( null != $min_id ) { + $where .= ' AND ID >= ' . intval( $min_id ); + } + + if ( null != $max_id ) { + $where .= ' AND ID <= ' . intval( $max_id ); + } + + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE $where" ); + } + + // TODO: actually use max_id/min_id + public function get_posts( $status = null, $min_id = null, $max_id = null ) { + $args = array( + 'orderby' => 'ID', + 'posts_per_page' => -1, + ); + + if ( $status ) { + $args['post_status'] = $status; + } else { + $args['post_status'] = 'any'; + } + + return get_posts( $args ); + } + + public function get_post( $id ) { + return get_post( $id ); + } + + public function upsert_post( $post, $silent = false ) { + global $wpdb; + + // reject the post if it's not a WP_Post + if ( ! $post instanceof WP_Post ) { + return; + } + + $post = $post->to_array(); + + // reject posts without an ID + if ( ! isset( $post['ID'] ) ) { + return; + } + + $now = current_time( 'mysql' ); + $now_gmt = get_gmt_from_date( $now ); + + $defaults = array( + 'ID' => 0, + 'post_author' => '0', + 'post_content' => '', + 'post_content_filtered' => '', + 'post_title' => '', + 'post_name' => '', + 'post_excerpt' => '', + 'post_status' => 'draft', + 'post_type' => 'post', + 'comment_status' => 'closed', + 'comment_count' => '0', + 'ping_status' => '', + 'post_password' => '', + 'to_ping' => '', + 'pinged' => '', + 'post_parent' => 0, + 'menu_order' => 0, + 'guid' => '', + 'post_date' => $now, + 'post_date_gmt' => $now_gmt, + 'post_modified' => $now, + 'post_modified_gmt' => $now_gmt, + ); + + $post = array_intersect_key( $post, $defaults ); + + $post = sanitize_post( $post, 'db' ); + + unset( $post['filter'] ); + + $exists = $wpdb->get_var( $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM $wpdb->posts WHERE ID = %d )", $post['ID'] ) ); + + if ( $exists ) { + $wpdb->update( $wpdb->posts, $post, array( 'ID' => $post['ID'] ) ); + } else { + $wpdb->insert( $wpdb->posts, $post ); + } + + clean_post_cache( $post['ID'] ); + } + + public function delete_post( $post_id ) { + wp_delete_post( $post_id, true ); + } + + public function posts_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->posts, Jetpack_Sync_Defaults::$default_post_checksum_columns, 'ID', Jetpack_Sync_Settings::get_blacklisted_post_types_sql(), $min_id, $max_id ); + } + + public function post_meta_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->postmeta, Jetpack_Sync_Defaults::$default_post_meta_checksum_columns, 'meta_id', Jetpack_Sync_Settings::get_whitelisted_post_meta_sql(), $min_id, $max_id ); + } + + public function comment_count( $status = null, $min_id = null, $max_id = null ) { + global $wpdb; + + $comment_approved = $this->comment_status_to_approval_value( $status ); + + if ( $comment_approved !== false ) { + $where = "comment_approved = '" . esc_sql( $comment_approved ) . "'"; + } else { + $where = '1=1'; + } + + if ( $min_id != null ) { + $where .= ' AND comment_ID >= ' . intval( $min_id ); + } + + if ( $max_id != null ) { + $where .= ' AND comment_ID <= ' . intval( $max_id ); + } + + return $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE $where" ); + } + + private function comment_status_to_approval_value( $status ) { + switch ( $status ) { + case 'approve': + return '1'; + case 'hold': + return '0'; + case 'spam': + return 'spam'; + case 'trash': + return 'trash'; + case 'any': + return false; + case 'all': + return false; + default: + return false; + } + } + + // TODO: actually use max_id/min_id + public function get_comments( $status = null, $min_id = null, $max_id = null ) { + $args = array( + 'orderby' => 'ID', + 'status' => 'all', + ); + + if ( $status ) { + $args['status'] = $status; + } + + return get_comments( $args ); + } + + public function get_comment( $id ) { + return WP_Comment::get_instance( $id ); + } + + public function upsert_comment( $comment ) { + global $wpdb; + + $comment = $comment->to_array(); + + // filter by fields on comment table + $comment_fields_whitelist = array( + 'comment_ID', + 'comment_post_ID', + 'comment_author', + 'comment_author_email', + 'comment_author_url', + 'comment_author_IP', + 'comment_date', + 'comment_date_gmt', + 'comment_content', + 'comment_karma', + 'comment_approved', + 'comment_agent', + 'comment_type', + 'comment_parent', + 'user_id', + ); + + foreach ( $comment as $key => $value ) { + if ( ! in_array( $key, $comment_fields_whitelist ) ) { + unset( $comment[ $key ] ); + } + } + + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS( SELECT 1 FROM $wpdb->comments WHERE comment_ID = %d )", + $comment['comment_ID'] + ) + ); + + if ( $exists ) { + $wpdb->update( $wpdb->comments, $comment, array( 'comment_ID' => $comment['comment_ID'] ) ); + } else { + $wpdb->insert( $wpdb->comments, $comment ); + } + + wp_update_comment_count( $comment['comment_post_ID'] ); + } + + public function trash_comment( $comment_id ) { + wp_delete_comment( $comment_id ); + } + + public function delete_comment( $comment_id ) { + wp_delete_comment( $comment_id, true ); + } + + public function spam_comment( $comment_id ) { + wp_spam_comment( $comment_id ); + } + + public function trashed_post_comments( $post_id, $statuses ) { + wp_trash_post_comments( $post_id ); + } + + public function untrashed_post_comments( $post_id ) { + wp_untrash_post_comments( $post_id ); + } + + public function comments_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->comments, Jetpack_Sync_Defaults::$default_comment_checksum_columns, 'comment_ID', Jetpack_Sync_Settings::get_comments_filter_sql(), $min_id, $max_id ); + } + + public function comment_meta_checksum( $min_id = null, $max_id = null ) { + global $wpdb; + return $this->table_checksum( $wpdb->commentmeta, Jetpack_Sync_Defaults::$default_comment_meta_checksum_columns, 'meta_id', Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql(), $min_id, $max_id ); + } + + public function options_checksum() { + global $wpdb; + + $options_whitelist = "'" . implode( "', '", Jetpack_Sync_Defaults::$default_options_whitelist ) . "'"; + $where_sql = "option_name IN ( $options_whitelist )"; + + return $this->table_checksum( $wpdb->options, Jetpack_Sync_Defaults::$default_option_checksum_columns, null, $where_sql, null, null ); + } + + + public function update_option( $option, $value ) { + return update_option( $option, $value ); + } + + public function get_option( $option, $default = false ) { + return get_option( $option, $default ); + } + + public function delete_option( $option ) { + return delete_option( $option ); + } + + public function set_theme_support( $theme_support ) { + // noop + } + + public function current_theme_supports( $feature ) { + return current_theme_supports( $feature ); + } + + public function get_metadata( $type, $object_id, $meta_key = '', $single = false ) { + return get_metadata( $type, $object_id, $meta_key, $single ); + } + + /** + * + * Stores remote meta key/values alongside an ID mapping key + * + * @param $type + * @param $object_id + * @param $meta_key + * @param $meta_value + * @param $meta_id + * + * @return bool + */ + public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ) { + + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + + global $wpdb; + + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS( SELECT 1 FROM $table WHERE meta_id = %d )", + $meta_id + ) + ); + + if ( $exists ) { + $wpdb->update( + $table, + array( + 'meta_key' => $meta_key, + 'meta_value' => maybe_serialize( $meta_value ), + ), + array( 'meta_id' => $meta_id ) + ); + } else { + $object_id_field = $type . '_id'; + $wpdb->insert( + $table, + array( + 'meta_id' => $meta_id, + $object_id_field => $object_id, + 'meta_key' => $meta_key, + 'meta_value' => maybe_serialize( $meta_value ), + ) + ); + } + + wp_cache_delete( $object_id, $type . '_meta' ); + + return true; + } + + public function delete_metadata( $type, $object_id, $meta_ids ) { + global $wpdb; + + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + + foreach ( $meta_ids as $meta_id ) { + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE meta_id = %d", $meta_id ) ); + } + + // if we don't have an object ID what do we do - invalidate ALL meta? + if ( $object_id ) { + wp_cache_delete( $object_id, $type . '_meta' ); + } + } + + // todo: test this out to make sure it works as expected. + public function delete_batch_metadata( $type, $object_ids, $meta_key ) { + global $wpdb; + + $table = _get_meta_table( $type ); + if ( ! $table ) { + return false; + } + $column = sanitize_key( $type . '_id' ); + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE $column IN (%s) && meta_key = %s", implode( ',', $object_ids ), $meta_key ) ); + + // if we don't have an object ID what do we do - invalidate ALL meta? + foreach ( $object_ids as $object_id ) { + wp_cache_delete( $object_id, $type . '_meta' ); + } + } + + // constants + public function get_constant( $constant ) { + $value = get_option( 'jetpack_constant_' . $constant ); + + if ( $value ) { + return $value; + } + + return null; + } + + public function set_constant( $constant, $value ) { + update_option( 'jetpack_constant_' . $constant, $value ); + } + + public function get_updates( $type ) { + $all_updates = get_option( 'jetpack_updates', array() ); + + if ( isset( $all_updates[ $type ] ) ) { + return $all_updates[ $type ]; + } else { + return null; + } + } + + public function set_updates( $type, $updates ) { + $all_updates = get_option( 'jetpack_updates', array() ); + $all_updates[ $type ] = $updates; + update_option( 'jetpack_updates', $all_updates ); + } + + // functions + public function get_callable( $name ) { + $value = get_option( 'jetpack_' . $name ); + + if ( $value ) { + return $value; + } + + return null; + } + + public function set_callable( $name, $value ) { + update_option( 'jetpack_' . $name, $value ); + } + + // network options + public function get_site_option( $option ) { + return get_option( 'jetpack_network_' . $option ); + } + + public function update_site_option( $option, $value ) { + return update_option( 'jetpack_network_' . $option, $value ); + } + + public function delete_site_option( $option ) { + return delete_option( 'jetpack_network_' . $option ); + } + + // terms + // terms + public function get_terms( $taxonomy ) { + return get_terms( $taxonomy ); + } + + public function get_term( $taxonomy, $term_id, $is_term_id = true ) { + $t = $this->ensure_taxonomy( $taxonomy ); + if ( ! $t || is_wp_error( $t ) ) { + return $t; + } + + return get_term( $term_id, $taxonomy ); + } + + private function ensure_taxonomy( $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + // try re-registering synced taxonomies + $taxonomies = $this->get_callable( 'taxonomies' ); + if ( ! isset( $taxonomies[ $taxonomy ] ) ) { + // doesn't exist, or somehow hasn't been synced + return new WP_Error( 'invalid_taxonomy', "The taxonomy '$taxonomy' doesn't exist" ); + } + $t = $taxonomies[ $taxonomy ]; + + return register_taxonomy( + $taxonomy, + $t->object_type, + (array) $t + ); + } + + return true; + } + + public function get_the_terms( $object_id, $taxonomy ) { + return get_the_terms( $object_id, $taxonomy ); + } + + public function update_term( $term_object ) { + $taxonomy = $term_object->taxonomy; + global $wpdb; + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS( SELECT 1 FROM $wpdb->terms WHERE term_id = %d )", + $term_object->term_id + ) + ); + if ( ! $exists ) { + $term_object = sanitize_term( clone( $term_object ), $taxonomy, 'db' ); + $term = array( + 'term_id' => $term_object->term_id, + 'name' => $term_object->name, + 'slug' => $term_object->slug, + 'term_group' => $term_object->term_group, + ); + $term_taxonomy = array( + 'term_taxonomy_id' => $term_object->term_taxonomy_id, + 'term_id' => $term_object->term_id, + 'taxonomy' => $term_object->taxonomy, + 'description' => $term_object->description, + 'parent' => (int) $term_object->parent, + 'count' => (int) $term_object->count, + ); + $wpdb->insert( $wpdb->terms, $term ); + $wpdb->insert( $wpdb->term_taxonomy, $term_taxonomy ); + + return true; + } + + return wp_update_term( $term_object->term_id, $taxonomy, (array) $term_object ); + } + + public function delete_term( $term_id, $taxonomy ) { + return wp_delete_term( $term_id, $taxonomy ); + } + + public function update_object_terms( $object_id, $taxonomy, $terms, $append ) { + wp_set_object_terms( $object_id, $terms, $taxonomy, $append ); + } + + public function delete_object_terms( $object_id, $tt_ids ) { + global $wpdb; + + if ( is_array( $tt_ids ) && ! empty( $tt_ids ) ) { + // escape + $tt_ids_sanitized = array_map( 'intval', $tt_ids ); + + $taxonomies = array(); + foreach ( $tt_ids_sanitized as $tt_id ) { + $term = get_term_by( 'term_taxonomy_id', $tt_id ); + $taxonomies[ $term->taxonomy ][] = $tt_id; + } + $in_tt_ids = implode( ', ', $tt_ids_sanitized ); + + /** + * Fires immediately before an object-term relationship is deleted. + * + * @since 2.9.0 + * + * @param int $object_id Object ID. + * @param array $tt_ids An array of term taxonomy IDs. + */ + do_action( 'delete_term_relationships', $object_id, $tt_ids_sanitized ); + $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_relationships WHERE object_id = %d AND term_taxonomy_id IN ($in_tt_ids)", $object_id ) ); + foreach ( $taxonomies as $taxonomy => $taxonomy_tt_ids ) { + $this->ensure_taxonomy( $taxonomy ); + wp_cache_delete( $object_id, $taxonomy . '_relationships' ); + /** + * Fires immediately after an object-term relationship is deleted. + * + * @since 2.9.0 + * + * @param int $object_id Object ID. + * @param array $tt_ids An array of term taxonomy IDs. + */ + do_action( 'deleted_term_relationships', $object_id, $taxonomy_tt_ids ); + wp_update_term_count( $taxonomy_tt_ids, $taxonomy ); + } + + return (bool) $deleted; + } + + return false; + } + + // users + public function user_count() { + + } + + public function get_user( $user_id ) { + return WP_User::get_instance( $user_id ); + } + + public function upsert_user( $user ) { + $this->invalid_call(); + } + + public function delete_user( $user_id ) { + $this->invalid_call(); + } + + public function upsert_user_locale( $user_id, $local ) { + $this->invalid_call(); + } + + public function delete_user_locale( $user_id ) { + $this->invalid_call(); + } + + public function get_user_locale( $user_id ) { + return get_user_locale( $user_id ); + } + + public function get_allowed_mime_types( $user_id ) { + + } + + public function checksum_all() { + $post_meta_checksum = $this->checksum_histogram( 'post_meta', 1 ); + $comment_meta_checksum = $this->checksum_histogram( 'comment_meta', 1 ); + + return array( + 'posts' => $this->posts_checksum(), + 'comments' => $this->comments_checksum(), + 'post_meta' => reset( $post_meta_checksum ), + 'comment_meta' => reset( $comment_meta_checksum ), + ); + } + + function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null, $columns = null, $strip_non_ascii = true, $salt = '' ) { + global $wpdb; + + $wpdb->queries = array(); + + switch ( $object_type ) { + case 'posts': + $object_count = $this->post_count( null, $start_id, $end_id ); + $object_table = $wpdb->posts; + $id_field = 'ID'; + $where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql(); + if ( empty( $columns ) ) { + $columns = Jetpack_Sync_Defaults::$default_post_checksum_columns; + } + break; + case 'post_meta': + $object_table = $wpdb->postmeta; + $where_sql = Jetpack_Sync_Settings::get_whitelisted_post_meta_sql(); + $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id ); + $id_field = 'meta_id'; + + if ( empty( $columns ) ) { + $columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns; + } + break; + case 'comments': + $object_count = $this->comment_count( null, $start_id, $end_id ); + $object_table = $wpdb->comments; + $id_field = 'comment_ID'; + $where_sql = Jetpack_Sync_Settings::get_comments_filter_sql(); + if ( empty( $columns ) ) { + $columns = Jetpack_Sync_Defaults::$default_comment_checksum_columns; + } + break; + case 'comment_meta': + $object_table = $wpdb->commentmeta; + $where_sql = Jetpack_Sync_Settings::get_whitelisted_comment_meta_sql(); + $object_count = $this->meta_count( $object_table, $where_sql, $start_id, $end_id ); + $id_field = 'meta_id'; + if ( empty( $columns ) ) { + $columns = Jetpack_Sync_Defaults::$default_post_meta_checksum_columns; + } + break; + default: + return false; + } + + $bucket_size = intval( ceil( $object_count / $buckets ) ); + $previous_max_id = 0; + $histogram = array(); + + $where = '1=1'; + + if ( $start_id ) { + $where .= " AND $id_field >= " . intval( $start_id ); + } + + if ( $end_id ) { + $where .= " AND $id_field <= " . intval( $end_id ); + } + + do { + list( $first_id, $last_id ) = $wpdb->get_row( + "SELECT MIN($id_field) as min_id, MAX($id_field) as max_id FROM ( SELECT $id_field FROM $object_table WHERE $where AND $id_field > $previous_max_id ORDER BY $id_field ASC LIMIT $bucket_size ) as ids", + ARRAY_N + ); + + if ( null === $first_id || null === $last_id ) { + // Nothing to checksum here... + break; + } + + // get the checksum value + $value = $this->table_checksum( $object_table, $columns, $id_field, $where_sql, $first_id, $last_id, $strip_non_ascii, $salt ); + + if ( is_wp_error( $value ) ) { + return $value; + } + + if ( null === $first_id || null === $last_id ) { + break; + } elseif ( $first_id === $last_id ) { + $histogram[ $first_id ] = $value; + } else { + $histogram[ "{$first_id}-{$last_id}" ] = $value; + } + + $previous_max_id = $last_id; + } while ( true ); + + return $histogram; + } + + private function table_checksum( $table, $columns, $id_column, $where_sql = '1=1', $min_id = null, $max_id = null, $strip_non_ascii = true, $salt = '' ) { + global $wpdb; + + // sanitize to just valid MySQL column names + $sanitized_columns = preg_grep( '/^[0-9,a-z,A-Z$_]+$/i', $columns ); + + if ( $strip_non_ascii ) { + $columns_sql = implode( ',', array_map( array( $this, 'strip_non_ascii_sql' ), $sanitized_columns ) ); + } else { + $columns_sql = implode( ',', $sanitized_columns ); + } + + if ( null !== $min_id && null !== $max_id ) { + if ( $min_id === $max_id ) { + $min_id = intval( $min_id ); + $where_sql .= " AND $id_column = $min_id LIMIT 1"; + } else { + $min_id = intval( $min_id ); + $max_id = intval( $max_id ); + $size = $max_id - $min_id; + $where_sql .= " AND $id_column >= $min_id AND $id_column <= $max_id LIMIT $size"; + } + } else { + if ( null !== $min_id ) { + $min_id = intval( $min_id ); + $where_sql .= " AND $id_column >= $min_id"; + } + + if ( null !== $max_id ) { + $max_id = intval( $max_id ); + $where_sql .= " AND $id_column <= $max_id"; + } + } + + $query = <<<ENDSQL + SELECT CAST( SUM( CRC32( CONCAT_WS( '#', '%s', {$columns_sql} ) ) ) AS UNSIGNED INT ) + FROM $table + WHERE $where_sql; +ENDSQL; + $result = $wpdb->get_var( $wpdb->prepare( $query, $salt ) ); + if ( $wpdb->last_error ) { + return new WP_Error( 'database_error', $wpdb->last_error ); + } + + return $result; + } + + public function get_checksum_type() { + return 'sum'; + } + + private function meta_count( $table, $where_sql, $min_id, $max_id ) { + global $wpdb; + + if ( $min_id != null ) { + $where_sql .= ' AND meta_id >= ' . intval( $min_id ); + } + + if ( $max_id != null ) { + $where_sql .= ' AND meta_id <= ' . intval( $max_id ); + } + + return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where_sql" ); + } + + /** + * Wraps a column name in SQL which strips non-ASCII chars. + * This helps normalize data to avoid checksum differences caused by + * badly encoded data in the DB + */ + function strip_non_ascii_sql( $column_name ) { + return "REPLACE( CONVERT( $column_name USING ascii ), '?', '' )"; + } + + private function invalid_call() { + $backtrace = debug_backtrace(); + $caller = $backtrace[1]['function']; + throw new Exception( "This function $caller is not supported on the WP Replicastore" ); + } +} 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..c22f4e51 --- /dev/null +++ b/plugins/jetpack/sync/interface.jetpack-sync-replicastore.php @@ -0,0 +1,140 @@ +<?php +/** + * Sync architecture prototype + * + * @author Dan Walmsley + * To run tests: phpunit --testsuite sync --filter New_Sync + */ + +/** + * A high-level interface for objects that store synced WordPress data + * Useful for ensuring that different storage mechanisms implement the + * required semantics for storing all the data that we sync + */ +interface iJetpack_Sync_Replicastore { + // remove all data + public function reset(); + + // trigger setup for sync start/end + public function full_sync_start( $config ); + + public function full_sync_end( $checksum ); + + // posts + public function post_count( $status = null, $min_id = null, $max_id = null ); + + public function get_posts( $status = null, $min_id = null, $max_id = null ); + + public function get_post( $id ); + + public function upsert_post( $post, $silent = false ); + + public function delete_post( $post_id ); + + public function posts_checksum( $min_id = null, $max_id = null ); + public function post_meta_checksum( $min_id = null, $max_id = null ); + + // comments + public function comment_count( $status = null, $min_id = null, $max_id = null ); + + public function get_comments( $status = null, $min_id = null, $max_id = null ); + + public function get_comment( $id ); + + public function upsert_comment( $comment ); + + public function trash_comment( $comment_id ); + + public function spam_comment( $comment_id ); + + public function delete_comment( $comment_id ); + + public function trashed_post_comments( $post_id, $statuses ); + + public function untrashed_post_comments( $post_id ); + + public function comments_checksum( $min_id = null, $max_id = null ); + public function comment_meta_checksum( $min_id = null, $max_id = null ); + + // options + public function update_option( $option, $value ); + + public function get_option( $option, $default = false ); + + public function delete_option( $option ); + + // themes + public function set_theme_support( $theme_support ); + + public function current_theme_supports( $feature ); + + // meta + public function get_metadata( $type, $object_id, $meta_key = '', $single = false ); + + public function upsert_metadata( $type, $object_id, $meta_key, $meta_value, $meta_id ); + + public function delete_metadata( $type, $object_id, $meta_ids ); + + public function delete_batch_metadata( $type, $object_ids, $meta_key ); + + // constants + public function get_constant( $constant ); + + public function set_constant( $constant, $value ); + + // updates + public function get_updates( $type ); + + public function set_updates( $type, $updates ); + + // functions + public function get_callable( $callable ); + + public function set_callable( $callable, $value ); + + // network options + public function get_site_option( $option ); + + public function update_site_option( $option, $value ); + + public function delete_site_option( $option ); + + // terms + public function get_terms( $taxonomy ); + + public function get_term( $taxonomy, $term_id, $is_term_id = true ); + + public function update_term( $term_object ); + + public function delete_term( $term_id, $taxonomy ); + + public function get_the_terms( $object_id, $taxonomy ); + + public function update_object_terms( $object_id, $taxonomy, $terms, $append ); + + public function delete_object_terms( $object_id, $tt_ids ); + + // users + public function user_count(); + + public function get_user( $user_id ); + + public function upsert_user( $user ); + + public function delete_user( $user_id ); + + public function upsert_user_locale( $user_id, $locale ); + + public function delete_user_locale( $user_id ); + + public function get_user_locale( $user_id ); + + public function get_allowed_mime_types( $user_id ); + + + // full checksum + public function checksum_all(); + + // histogram + public function checksum_histogram( $object_type, $buckets, $start_id = null, $end_id = null ); +} |