Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.57% covered (warning)
68.57%
746 / 1088
44.16% covered (danger)
44.16%
34 / 77
CRAP
0.00% covered (danger)
0.00%
0 / 1
Manager
68.57% covered (warning)
68.57%
746 / 1088
44.16% covered (danger)
44.16%
34 / 77
4432.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 configure
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
4
 add_connection_status_invalidation_hooks
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 setup_xmlrpc_handlers
14.81% covered (danger)
14.81%
4 / 27
0.00% covered (danger)
0.00%
0 / 1
85.80
 initialize_rest_api_registration_connector
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 alternate_xmlrpc
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 remove_non_jetpack_xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 require_jetpack_authentication
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 authenticate_jetpack
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 verify_xml_rpc_signature
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 internal_verify_xml_rpc_signature
72.90% covered (warning)
72.90%
78 / 107
0.00% covered (danger)
0.00%
0 / 1
52.39
 get_current_request_transport
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
10
 build_connection_error_data
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 is_active
n/a
0 / 0
n/a
0 / 0
1
 get_tokens
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_registered
n/a
0 / 0
n/a
0 / 0
1
 is_connected
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 reset_connection_status
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 has_connected_admin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_connected_user
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_connected_users
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 has_connected_owner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_userless
n/a
0 / 0
n/a
0 / 0
1
 is_site_connection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 is_missing_connection_owner
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 is_user_connected
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 get_connection_owner_id
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 get_connected_user_data
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
 get_connected_site_data
93.55% covered (success)
93.55%
29 / 31
0.00% covered (danger)
0.00%
0 / 1
12.04
 get_connection_owner
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 is_connection_owner
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_ownership_transferable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect_user
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 disconnect_user_force
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
6.60
 disconnect_all_users_except_primary
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 disconnect_user
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
11.24
 unlink_user_from_wpcom
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 update_connection_owner
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
5
 update_connection_owner_wpcom
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 api_url
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 xmlrpc_api_url
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 register
81.90% covered (warning)
81.90%
86 / 105
0.00% covered (danger)
0.00%
0 / 1
16.33
 try_registration
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
6.20
 add_register_request_param
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 validate_remote_register_response
29.17% covered (danger)
29.17%
14 / 48
0.00% covered (danger)
0.00%
0 / 1
73.06
 add_nonce
n/a
0 / 0
n/a
0 / 0
1
 clean_nonces
n/a
0 / 0
n/a
0 / 0
2
 jetpack_connection_custom_caps
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 get_max_execution_time
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 set_min_time_limit
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 get_assumed_site_creation_date
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
3
 apply_activation_source_to_args
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 generate_secrets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_secrets
n/a
0 / 0
n/a
0 / 0
1
 delete_secrets
n/a
0 / 0
n/a
0 / 0
1
 delete_all_connection_tokens
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
5.00
 disconnect_site_wpcom
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
7.07
 remove_connection
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reconnect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 restore
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
10.02
 handle_registration
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validate_tokens
n/a
0 / 0
n/a
0 / 0
1
 verify_secrets
n/a
0 / 0
n/a
0 / 0
1
 handle_authorization
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_token
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_authorization_url
96.36% covered (success)
96.36%
53 / 55
0.00% covered (danger)
0.00%
0 / 1
9
 authorize
68.42% covered (warning)
68.42%
26 / 38
0.00% covered (danger)
0.00%
0 / 1
16.53
 disconnect_site
60.87% covered (warning)
60.87%
14 / 23
0.00% covered (danger)
0.00%
0 / 1
11.83
 sha1_base64
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_usable_domain
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
90
 get_access_token
n/a
0 / 0
n/a
0 / 0
1
 xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reset_raw_post_data
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 public_xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 jetpack_get_options
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
12
 xmlrpc_options
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 reset_saved_auth_state
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sign_role
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_plugin_instance
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_plugin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_connected_plugins
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 disable_plugin
n/a
0 / 0
n/a
0 / 0
1
 enable_plugin
n/a
0 / 0
n/a
0 / 0
1
 is_plugin_enabled
n/a
0 / 0
n/a
0 / 0
1
 refresh_blog_token
76.67% covered (warning)
76.67%
23 / 30
0.00% covered (danger)
0.00%
0 / 1
10.03
 refresh_user_token
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_signed_token
n/a
0 / 0
n/a
0 / 0
1
 add_stats_to_heartbeat
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
7.32
 track_xmlrpc_error
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 get_site_id
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 is_ready_for_cleanup
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * The Jetpack Connection manager class file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Automattic\Jetpack\A8c_Mc_Stats;
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Heartbeat;
13use Automattic\Jetpack\Identity_Crisis;
14use Automattic\Jetpack\Partner;
15use Automattic\Jetpack\Roles;
16use Automattic\Jetpack\Status;
17use Automattic\Jetpack\Status\Host;
18use Automattic\Jetpack\Terms_Of_Service;
19use Automattic\Jetpack\Tracking;
20use IXR_Error;
21use Jetpack_IXR_Client;
22use Jetpack_Options;
23use Jetpack_XMLRPC_Server;
24use WP_Error;
25use WP_User;
26
27/**
28 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
29 * and Jetpack.
30 */
31class Manager {
32    /**
33     * A copy of the raw POST data for signature verification purposes.
34     *
35     * @var string
36     */
37    protected $raw_post_data;
38
39    /**
40     * Verification data needs to be stored to properly verify everything.
41     *
42     * @var Object
43     */
44    private $xmlrpc_verification = null;
45
46    /**
47     * Plugin management object.
48     *
49     * @var Plugin
50     */
51    private $plugin = null;
52
53    /**
54     * Error handler object.
55     *
56     * @var Error_Handler
57     */
58    public $error_handler = null;
59
60    /**
61     * Jetpack_XMLRPC_Server object
62     *
63     * @var Jetpack_XMLRPC_Server
64     */
65    public $xmlrpc_server = null;
66
67    /**
68     * Holds extra parameters that will be sent along in the register request body.
69     *
70     * Use Manager::add_register_request_param to add values to this array.
71     *
72     * @since 1.26.0
73     * @var array
74     */
75    private static $extra_register_params = array();
76
77    /**
78     * We store ID's of users already disconnected to prevent multiple disconnect requests.
79     *
80     * @var array
81     */
82    private static $disconnected_users = array();
83
84    /**
85     * Cached connection status.
86     *
87     * @var bool|null True if the site is connected, false if not, null if not determined yet.
88     */
89    private static $is_connected = null;
90
91    /**
92     * Memoized user ID of the connection owner.
93     * If undefined or invalid, set to 0.
94     *
95     * @var null|int
96     */
97    private static $connection_owner_id = null;
98
99    /**
100     * Tracks whether connection status invalidation hooks have been added.
101     *
102     * @var bool
103     */
104    private static $connection_invalidators_added = false;
105
106    /**
107     * Initialize the object.
108     * Make sure to call the "Configure" first.
109     *
110     * @param string $plugin_slug Slug of the plugin using the connection (optional, but encouraged).
111     *
112     * @see \Automattic\Jetpack\Config
113     */
114    public function __construct( $plugin_slug = null ) {
115        if ( $plugin_slug && is_string( $plugin_slug ) ) {
116            $this->set_plugin_instance( new Plugin( $plugin_slug ) );
117        }
118    }
119
120    /**
121     * Initializes required listeners. This is done separately from the constructors
122     * because some objects sometimes need to instantiate separate objects of this class.
123     *
124     * @todo Implement a proper nonce verification.
125     */
126    public static function configure() {
127        $manager = new self();
128
129        add_filter(
130            'jetpack_constant_default_value',
131            __NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
132            10,
133            2
134        );
135
136        $manager->setup_xmlrpc_handlers(
137            null,
138            $manager->has_connected_owner(),
139            $manager->verify_xml_rpc_signature()
140        );
141
142        $manager->error_handler = Error_Handler::get_instance();
143
144        if ( $manager->is_connected() ) {
145            add_filter( 'xmlrpc_methods', array( $manager, 'public_xmlrpc_methods' ) );
146            add_filter( 'shutdown', array( Package_Version_Tracker::class, 'update_on_shutdown' ) );
147        }
148
149        // This runs on priority 11 - at least one api method in the connection package is set to override a previously
150        // existing method from the Jetpack plugin. Running later than Jetpack's api init ensures the override is successful.
151        add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ), 11 );
152
153        ( new Nonce_Handler() )->init_schedule();
154
155        add_action( 'plugins_loaded', __NAMESPACE__ . '\Plugin_Storage::configure', 100 );
156
157        add_filter( 'map_meta_cap', array( $manager, 'jetpack_connection_custom_caps' ), 1, 4 );
158
159        Heartbeat::init();
160        add_filter( 'jetpack_heartbeat_stats_array', array( $manager, 'add_stats_to_heartbeat' ) );
161        add_action( 'jetpack_verify_signature_error', array( $manager, 'track_xmlrpc_error' ) );
162
163        Webhooks::init( $manager );
164
165        // Unlink user before deleting the user from WP.com.
166        add_action( 'deleted_user', array( $manager, 'disconnect_user_force' ), 9, 1 );
167        add_action( 'remove_user_from_blog', array( $manager, 'disconnect_user_force' ), 9, 1 );
168
169        // Add hooks for cleaning up account mismatch transients
170        $user_account_status = new User_Account_Status();
171        add_action( 'delete_user', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 );
172        add_action( 'remove_user_from_blog', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 );
173        add_action( 'user_register', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 );
174        add_action( 'profile_update', array( $user_account_status, 'clean_account_mismatch_transients' ), 9, 1 );
175
176        $manager->add_connection_status_invalidation_hooks();
177
178        // Set up package version hook.
179        add_filter( 'jetpack_package_versions', __NAMESPACE__ . '\Package_Version::send_package_version_to_tracker' );
180
181        if ( defined( 'JETPACK__SANDBOX_DOMAIN' ) && JETPACK__SANDBOX_DOMAIN ) {
182            ( new Server_Sandbox() )->init();
183        }
184
185        // Initialize connection notices.
186        new Connection_Notice();
187
188        // Initialize token locks.
189        new Tokens_Locks();
190
191        // Initial Partner management.
192        Partner::init();
193
194        // WP 7.0+ Connectors screen card.
195        Jetpack_Connector::init();
196
197        // Site Health integration.
198        Site_Health::init();
199    }
200
201    /**
202     * Adds hooks to invalidate the memoized connection status.
203     */
204    private function add_connection_status_invalidation_hooks() {
205        if ( self::$connection_invalidators_added ) {
206            return;
207        }
208
209        // Force is_connected() to recompute after important actions.
210        add_action( 'jetpack_site_registered', array( $this, 'reset_connection_status' ) );
211        add_action( 'jetpack_site_disconnected', array( $this, 'reset_connection_status' ) );
212        add_action( 'jetpack_sync_register_user', array( $this, 'reset_connection_status' ) );
213        add_action( 'pre_update_jetpack_option_id', array( $this, 'reset_connection_status' ) );
214        add_action( 'pre_update_jetpack_option_blog_token', array( $this, 'reset_connection_status' ) );
215        add_action( 'pre_update_jetpack_option_user_token', array( $this, 'reset_connection_status' ) );
216        add_action( 'pre_update_jetpack_option_user_tokens', array( $this, 'reset_connection_status' ) );
217        add_action( 'pre_update_jetpack_option_master_user', array( $this, 'reset_connection_status' ) );
218        // phpcs:ignore WPCUT.SwitchBlog.SwitchBlog -- wpcom flags **every** use of switch_blog, apparently expecting valid instances to ignore or suppress the sniff.
219        add_action( 'switch_blog', array( $this, 'reset_connection_status' ) );
220        add_action( 'jetpack_external_storage_provider_registered', array( $this, 'reset_connection_status' ), 10, 0 );
221
222        self::$connection_invalidators_added = true;
223    }
224
225    /**
226     * Sets up the XMLRPC request handlers.
227     *
228     * @since 1.25.0 Deprecate $is_active param.
229     * @since 2.8.4 Deprecate $request_params param.
230     *
231     * @param array|null            $deprecated Deprecated. Not used.
232     * @param bool                  $has_connected_owner Whether the site has a connected owner.
233     * @param bool                  $is_signed whether the signature check has been successful.
234     * @param Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
235     */
236    public function setup_xmlrpc_handlers(
237        $deprecated,
238        $has_connected_owner,
239        $is_signed,
240        ?Jetpack_XMLRPC_Server $xmlrpc_server = null
241    ) {
242        add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 );
243        if ( $deprecated !== null ) {
244            _deprecated_argument( __METHOD__, '2.8.4' );
245        }
246        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We are using the 'for' request param to early return unless it's 'jetpack'.
247        if ( ! isset( $_GET['for'] ) || 'jetpack' !== $_GET['for'] ) {
248            return false;
249        }
250
251        // Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
252        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This just determines whether to handle the request as an XML-RPC request. The actual XML-RPC endpoints do the appropriate nonce checking where applicable. Plus we make sure to clear all cookies via require_jetpack_authentication called later in method.
253        if ( isset( $_GET['jetpack'] ) && 'comms' === $_GET['jetpack'] ) {
254            if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
255                // Use the real constant here for WordPress' sake.
256                define( 'XMLRPC_REQUEST', true );
257            }
258
259            add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) );
260
261            add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 );
262        }
263
264        if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
265            return false;
266        }
267
268        // Display errors can cause the XML to be not well formed.
269        // This only affects Jetpack XML-RPC endpoints received from WordPress.com servers.
270        // All other XML-RPC requests are unaffected.
271        @ini_set( 'display_errors', false ); // phpcs:ignore
272
273        if ( $xmlrpc_server ) {
274            $this->xmlrpc_server = $xmlrpc_server;
275        } else {
276            $this->xmlrpc_server = new Jetpack_XMLRPC_Server();
277        }
278
279        $this->require_jetpack_authentication();
280
281        if ( $is_signed ) {
282            // If the site is connected either at a site or user level and the request is signed, expose the methods.
283            // The callback is responsible to determine whether the request is signed with blog or user token and act accordingly.
284            // The actual API methods.
285            $callback = array( $this->xmlrpc_server, 'xmlrpc_methods' );
286
287            // Hack to preserve $HTTP_RAW_POST_DATA.
288            add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
289
290        } elseif ( $has_connected_owner ) {
291            // The jetpack.authorize method should be available for unauthenticated users on a site with an
292            // active Jetpack connection, so that additional users can link their account.
293            $callback = array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' );
294        } else {
295            // Any other unsigned request should expose the bootstrap methods.
296            $callback = array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' );
297            new XMLRPC_Connector( $this );
298        }
299
300        add_filter( 'xmlrpc_methods', $callback );
301
302        // Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on.
303        add_filter( 'pre_option_enable_xmlrpc', '__return_true' );
304        return true;
305    }
306
307    /**
308     * Initializes the REST API connector on the init hook.
309     */
310    public function initialize_rest_api_registration_connector() {
311        new REST_Connector( $this );
312    }
313
314    /**
315     * Since a lot of hosts use a hammer approach to "protecting" WordPress sites,
316     * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive
317     * security/firewall policies, we provide our own alternate XML RPC API endpoint
318     * which is accessible via a different URI. Most of the below is copied directly
319     * from /xmlrpc.php so that we're replicating it as closely as possible.
320     *
321     * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
322     *
323     * @return never
324     */
325    public function alternate_xmlrpc() {
326        // Some browser-embedded clients send cookies. We don't want them.
327        $_COOKIE = array();
328
329        include_once ABSPATH . 'wp-admin/includes/admin.php';
330        include_once ABSPATH . WPINC . '/class-IXR.php';
331        include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php';
332
333        /**
334         * Filters the class used for handling XML-RPC requests.
335         *
336         * @since 1.7.0
337         * @since-jetpack 3.1.0
338         *
339         * @param string $class The name of the XML-RPC server class.
340         */
341        $wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' );
342        $wp_xmlrpc_server       = new $wp_xmlrpc_server_class();
343
344        // Fire off the request.
345        nocache_headers();
346        $wp_xmlrpc_server->serve_request();
347
348        exit( 0 );
349    }
350
351    /**
352     * Removes all XML-RPC methods that are not `jetpack.*`.
353     * Only used in our alternate XML-RPC endpoint, where we want to
354     * ensure that Core and other plugins' methods are not exposed.
355     *
356     * @param array $methods a list of registered WordPress XMLRPC methods.
357     * @return array filtered $methods
358     */
359    public function remove_non_jetpack_xmlrpc_methods( $methods ) {
360        $jetpack_methods = array();
361
362        foreach ( $methods as $method => $callback ) {
363            if ( str_starts_with( $method, 'jetpack.' ) ) {
364                $jetpack_methods[ $method ] = $callback;
365            }
366        }
367
368        return $jetpack_methods;
369    }
370
371    /**
372     * Removes all other authentication methods not to allow other
373     * methods to validate unauthenticated requests.
374     */
375    public function require_jetpack_authentication() {
376        // Don't let anyone authenticate.
377        $_COOKIE = array();
378        remove_all_filters( 'authenticate' );
379        remove_all_actions( 'wp_login_failed' );
380
381        if ( $this->is_connected() ) {
382            // Allow Jetpack authentication.
383            add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 );
384        }
385    }
386
387    /**
388     * Authenticates XML-RPC and other requests from the Jetpack Server
389     *
390     * @param WP_User|mixed $user user object if authenticated.
391     * @param string        $username username.
392     * @param string        $password password string.
393     * @return WP_User|mixed authenticated user or error.
394     */
395    public function authenticate_jetpack( $user, $username, $password ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
396        if ( is_a( $user, '\\WP_User' ) ) {
397            return $user;
398        }
399
400        $token_details = $this->verify_xml_rpc_signature();
401
402        if ( ! $token_details ) {
403            return $user;
404        }
405
406        if ( 'user' !== $token_details['type'] ) {
407            return $user;
408        }
409
410        if ( ! $token_details['user_id'] ) {
411            return $user;
412        }
413
414        nocache_headers();
415
416        return new \WP_User( $token_details['user_id'] );
417    }
418
419    /**
420     * Verifies the signature of the current request.
421     *
422     * @return false|array
423     */
424    public function verify_xml_rpc_signature() {
425        if ( $this->xmlrpc_verification === null ) {
426            $this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature();
427
428            if ( is_wp_error( $this->xmlrpc_verification ) ) {
429                /**
430                 * Action for logging XMLRPC signature verification errors. This data is sensitive.
431                 *
432                 * @since 1.7.0
433                 * @since-jetpack 7.5.0
434                 *
435                 * @param WP_Error $signature_verification_error The verification error
436                 */
437                do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification );
438
439                Error_Handler::get_instance()->report_error( $this->xmlrpc_verification );
440
441            }
442        }
443
444        return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification;
445    }
446
447    /**
448     * Verifies the signature of the current request.
449     *
450     * This function has side effects and should not be used. Instead,
451     * use the memoized version `->verify_xml_rpc_signature()`.
452     *
453     * @internal
454     * @todo Refactor to use proper nonce verification.
455     */
456    private function internal_verify_xml_rpc_signature() {
457        // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
458        // It's not for us.
459        if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) {
460            return false;
461        }
462
463        // Skip XML-RPC signature verification for OAuth authorization flow.
464        // OAuth uses GET requests without body-hash and has its own
465        // signature verification in Authorize_Json_Api class.
466        if ( isset( $_GET['action'] ) && $_GET['action'] === 'jetpack_json_api_authorization' ) {
467            return false;
468        }
469
470        $signature_details = array(
471            'token'     => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '',
472            'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '',
473            'nonce'     => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '',
474            'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '',
475            'method'    => isset( $_SERVER['REQUEST_METHOD'] ) ? wp_unslash( $_SERVER['REQUEST_METHOD'] ) : null,
476            'url'       => wp_unslash( ( $_SERVER['HTTP_HOST'] ?? null ) . ( $_SERVER['REQUEST_URI'] ?? null ) ), // Temp - will get real signature URL later.
477            'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '',
478        );
479
480        // Transport of the incoming request being verified. This signature-verification path
481        // serves both XML-RPC requests and signed REST requests (REST_Authentication funnels
482        // REST authentication into verify_xml_rpc_signature()), so the stored error type is
483        // derived from the actual request context rather than hardcoded.
484        $error_type      = $this->get_current_request_transport();
485        $error_direction = 'incoming'; // Matches Error_Handler::DIRECTION_INCOMING â€” see build_connection_error_data() for why the constant is not referenced.
486
487        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
488        @list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) );
489        // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
490
491        $jetpack_api_version = Constants::get_constant( 'JETPACK__API_VERSION' );
492
493        if (
494            empty( $token_key )
495                || empty( $version )
496                || (string) $jetpack_api_version !== $version
497        ) {
498            return new \WP_Error( 'malformed_token', 'Malformed token in request', $this->build_connection_error_data( $signature_details, $error_type, $error_direction ) );
499        }
500
501        if ( '0' === $user_id ) {
502            $token_type = 'blog';
503            $user_id    = 0;
504        } else {
505            $token_type = 'user';
506            if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) {
507                return new \WP_Error(
508                    'malformed_user_id',
509                    'Malformed user_id in request',
510                    $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
511                );
512            }
513            $user_id = (int) $user_id;
514
515            $user = new \WP_User( $user_id );
516            if ( ! $user->exists() ) {
517                return new \WP_Error(
518                    'unknown_user',
519                    sprintf( 'User %d does not exist', $user_id ),
520                    $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
521                );
522            }
523        }
524
525        $token = $this->get_tokens()->get_access_token( $user_id, $token_key, false );
526        if ( is_wp_error( $token ) ) {
527            $token->add_data( $this->build_connection_error_data( $signature_details, $error_type, $error_direction ) );
528            return $token;
529        } elseif ( ! $token ) {
530            return new \WP_Error(
531                'unknown_token',
532                sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ),
533                $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
534            );
535        }
536
537        $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
538        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Used to verify a cryptographic signature of the post data. Also a nonce is verified later in the function.
539        if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
540            $post_data   = $_POST; // We need all of $_POST in order to verify a cryptographic signature of the post data.
541            $file_hashes = array();
542            foreach ( $post_data as $post_data_key => $post_data_value ) {
543                if ( ! str_starts_with( $post_data_key, '_jetpack_file_hmac_' ) ) {
544                    continue;
545                }
546                $post_data_key                 = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
547                $file_hashes[ $post_data_key ] = $post_data_value;
548            }
549
550            foreach ( $file_hashes as $post_data_key => $post_data_value ) {
551                unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] );
552                $post_data[ $post_data_key ] = $post_data_value;
553            }
554
555            ksort( $post_data );
556
557            $body = http_build_query( stripslashes_deep( $post_data ) );
558        } elseif ( $this->raw_post_data === null ) {
559            $body = file_get_contents( 'php://input' );
560        } else {
561            $body = null;
562        }
563        // phpcs:enable
564
565        $signature = $jetpack_signature->sign_current_request(
566            array( 'body' => $body === null ? $this->raw_post_data : $body )
567        );
568
569        $signature_details['url'] = $jetpack_signature->current_request_url;
570
571        if ( ! $signature ) {
572            return new \WP_Error(
573                'could_not_sign',
574                'Unknown signature error',
575                $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
576            );
577        } elseif ( is_wp_error( $signature ) ) {
578            // Jetpack_Signature errors carry their own signature_details (or, for some codes,
579            // no data at all) but never a type or direction; normalize them into the standard
580            // error data shape so Error_Handler can attribute and store them.
581            $signature_error_data = $signature->get_error_data();
582            if ( isset( $signature_error_data['signature_details'] ) && is_array( $signature_error_data['signature_details'] ) ) {
583                $signature_details = array_merge( $signature_details, $signature_error_data['signature_details'] );
584            }
585            $signature->add_data( $this->build_connection_error_data( $signature_details, $error_type, $error_direction ) );
586            return $signature;
587        }
588
589        // phpcs:disable WordPress.Security.NonceVerification.Recommended
590        $timestamp = (int) $_GET['timestamp'];
591        $nonce     = wp_unslash( (string) $_GET['nonce'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- WP Core doesn't sanitize nonces either.
592        // phpcs:enable WordPress.Security.NonceVerification.Recommended
593
594        // Use up the nonce regardless of whether the signature matches.
595        if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) {
596            return new \WP_Error(
597                'invalid_nonce',
598                'Could not add nonce',
599                $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
600            );
601        }
602
603        // Be careful about what you do with this debugging data.
604        // If a malicious requester has access to the expected signature,
605        // bad things might be possible.
606        $signature_details['expected'] = $signature;
607
608        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
609        if ( ! hash_equals( $signature, wp_unslash( $_GET['signature'] ) ) ) {
610            return new \WP_Error(
611                'signature_mismatch',
612                'Signature mismatch',
613                $this->build_connection_error_data( $signature_details, $error_type, $error_direction )
614            );
615        }
616
617        /**
618         * Action for additional token checking.
619         *
620         * @since 1.7.0
621         * @since-jetpack 7.7.0
622         *
623         * @param array $post_data request data.
624         * @param array $token_data token data.
625         */
626        return apply_filters(
627            'jetpack_signature_check_token',
628            array(
629                'type'      => $token_type,
630                'token_key' => $token_key,
631                'user_id'   => $token->external_user_id,
632            ),
633            $token,
634            $this->raw_post_data
635        );
636    }
637
638    /**
639     * Determines the transport of the incoming request currently being verified.
640     *
641     * @since 8.9.0
642     *
643     * @return string Error_Handler::ERROR_TYPE_XMLRPC or Error_Handler::ERROR_TYPE_REST.
644     */
645    private function get_current_request_transport() {
646        $is_xmlrpc = defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST;
647
648        // XMLRPC_REQUEST covers both /xmlrpc.php and the alternate XML-RPC endpoint, which
649        // defines the constant itself (see setup_xmlrpc_handlers). Outside those, signed REST
650        // requests are detected via the REST dispatch state. Anything else (e.g. signed
651        // requests verified on the 'authenticate' filter for regular URLs) keeps the historic
652        // XML-RPC label rather than guessing at a transport.
653        $is_rest = ! $is_xmlrpc && ( function_exists( 'wp_is_rest_endpoint' ) ? wp_is_rest_endpoint() : ( defined( 'REST_REQUEST' ) && REST_REQUEST ) );
654
655        // Signature verification can run before REST dispatch is set up: REST_Authentication
656        // hooks `determine_current_user`, which any plugin can trigger early (e.g. by calling
657        // wp_get_current_user() on plugins_loaded), before the REST_REQUEST constant exists.
658        // In that window, recognize REST requests by their URL: the REST prefix in the path,
659        // or the rest_route query argument used by sites without pretty permalinks.
660        if ( ! $is_xmlrpc && ! $is_rest ) {
661            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only used to classify the request transport.
662            $has_rest_route_arg = isset( $_GET['rest_route'] );
663            $request_path       = (string) wp_parse_url( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '', PHP_URL_PATH ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Parsed for path comparison only.
664            $is_rest            = $has_rest_route_arg || false !== strpos( $request_path, '/' . rest_get_url_prefix() . '/' );
665        }
666
667        // The literals match Error_Handler::ERROR_TYPE_REST / ERROR_TYPE_XMLRPC â€” see
668        // build_connection_error_data() for why the constants are not referenced.
669        return $is_rest ? 'rest' : 'xmlrpc';
670    }
671
672    /**
673     * Builds the standardized connection error data attached to signature-verification errors.
674     *
675     * Wraps `Error_Handler::build_connection_error_data()`, falling back to the legacy
676     * error-data shape when the loaded Error_Handler predates that method: during a plugin
677     * update, an older version of the class can already be in memory while this file is the
678     * new one, and a mid-update request must never fatal. For the same reason, code in this
679     * class must not reference Error_Handler constants introduced along with that method
680     * ('xmlrpc', 'rest', 'local_state', 'incoming', 'outgoing') â€” use the literal values.
681     *
682     * @since 8.10.0
683     *
684     * @param array  $signature_details Details of the request signature being verified.
685     * @param string $error_type        The transport of the request: 'xmlrpc' or 'rest'.
686     * @param string $error_direction   The direction of the request: 'incoming' or 'outgoing'.
687     * @return array Error data for `WP_Error`.
688     */
689    private function build_connection_error_data( $signature_details, $error_type, $error_direction ) {
690        if ( ! method_exists( Error_Handler::class, 'build_connection_error_data' ) ) {
691            return compact( 'signature_details', 'error_type' );
692        }
693        return Error_Handler::build_connection_error_data( $signature_details, $error_type, $error_direction );
694    }
695
696    /**
697     * Returns true if the current site is connected to WordPress.com and has the minimum requirements to enable Jetpack UI.
698     *
699     * This method is deprecated since version 1.25.0 of this package. Please use has_connected_owner instead.
700     *
701     * Since this method has a wide spread use, we decided not to throw any deprecation warnings for now.
702     *
703     * @deprecated 1.25.0
704     * @see Manager::has_connected_owner
705     * @return bool is the site connected?
706     */
707    public function is_active() {
708        return (bool) $this->get_tokens()->get_access_token( true );
709    }
710
711    /**
712     * Obtains an instance of the Tokens class.
713     *
714     * @return Tokens the Tokens object
715     */
716    public function get_tokens() {
717        return new Tokens();
718    }
719
720    /**
721     * Returns true if the site has both a token and a blog id, which indicates a site has been registered.
722     *
723     * @access public
724     * @deprecated 1.12.1 Use is_connected instead
725     * @see Manager::is_connected
726     *
727     * @return bool
728     */
729    public function is_registered() {
730        _deprecated_function( __METHOD__, '1.12.1' );
731        return $this->is_connected();
732    }
733
734    /**
735     * Returns true if the site has both a token and a blog id, which indicates a site has been connected.
736     *
737     * @access public
738     * @since 1.21.1
739     *
740     * @return bool
741     */
742    public function is_connected() {
743        if ( self::$is_connected === null ) {
744            if ( ! self::$connection_invalidators_added ) {
745                $this->add_connection_status_invalidation_hooks();
746            }
747
748            $has_blog_id = (bool) \Jetpack_Options::get_option( 'id' );
749            if ( $has_blog_id ) {
750                self::$is_connected = (bool) $this->get_tokens()->get_access_token();
751            } else {
752                // Short-circuit, no need to check for tokens if there's no blog ID.
753                self::$is_connected = false;
754            }
755        }
756        return self::$is_connected;
757    }
758
759    /**
760     * Resets the memoized connection status.
761     * This will force the connection status to be recomputed on the next check.
762     *
763     * @since 5.0.0
764     */
765    public function reset_connection_status() {
766        self::$is_connected        = null;
767        self::$connection_owner_id = null;
768    }
769
770    /**
771     * Returns true if the site has at least one connected administrator.
772     *
773     * @access public
774     * @since 1.21.1
775     *
776     * @return bool
777     */
778    public function has_connected_admin() {
779        return (bool) count( $this->get_connected_users( 'manage_options' ) );
780    }
781
782    /**
783     * Returns true if the site has any connected user.
784     *
785     * @access public
786     * @since 1.21.1
787     *
788     * @return bool
789     */
790    public function has_connected_user() {
791        return (bool) count( $this->get_connected_users( 'any', 1 ) );
792    }
793
794    /**
795     * Returns an array of users that have user tokens for communicating with wpcom.
796     * Able to select by specific capability.
797     *
798     * @since 9.9.1 Added $limit parameter.
799     *
800     * @param string   $capability The capability of the user.
801     * @param int|null $limit How many connected users to get before returning.
802     * @return WP_User[] Array of WP_User objects if found.
803     */
804    public function get_connected_users( $capability = 'any', $limit = null ) {
805        $connected_users = array();
806        $user_tokens     = $this->get_tokens()->get_user_tokens();
807
808        if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) {
809            return $connected_users;
810        }
811        $connected_user_ids = array_keys( $user_tokens );
812
813        if ( ! empty( $connected_user_ids ) ) {
814            foreach ( $connected_user_ids as $id ) {
815                // Check for capability.
816                if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
817                    continue;
818                }
819
820                $user_data = get_userdata( $id );
821                if ( $user_data instanceof \WP_User ) {
822                    $connected_users[] = $user_data;
823                    if ( $limit && count( $connected_users ) >= $limit ) {
824                        return $connected_users;
825                    }
826                }
827            }
828        }
829
830        return $connected_users;
831    }
832
833    /**
834     * Returns true if the site has a connected Blog owner (master_user).
835     *
836     * @access public
837     * @since 1.21.1
838     *
839     * @return bool
840     */
841    public function has_connected_owner() {
842        return (bool) $this->get_connection_owner_id();
843    }
844
845    /**
846     * Returns true if the site is connected only at a site level.
847     *
848     * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection.
849     *
850     * @access public
851     * @since 1.25.0
852     * @deprecated 1.27.0
853     *
854     * @return bool
855     */
856    public function is_userless() {
857        _deprecated_function( __METHOD__, '1.27.0', 'Automattic\\Jetpack\\Connection\\Manager::is_site_connection' );
858        return $this->is_site_connection();
859    }
860
861    /**
862     * Returns true if the site is connected only at a site level.
863     *
864     * Note that we are explicitly checking for the existence of the master_user option in order to account for cases where we don't have any user tokens (user-level connection) but the master_user option is set, which could be the result of a problematic user connection.
865     *
866     * @access public
867     * @since 1.27.0
868     *
869     * @return bool
870     */
871    public function is_site_connection() {
872        return $this->is_connected() && ! $this->has_connected_user() && ! \Jetpack_Options::get_option( 'master_user' );
873    }
874
875    /**
876     * Checks to see if the connection owner of the site is missing.
877     *
878     * @return bool
879     */
880    public function is_missing_connection_owner() {
881        $connection_owner = $this->get_connection_owner_id();
882        if ( ! get_user_by( 'id', $connection_owner ) ) {
883            return true;
884        }
885
886        return false;
887    }
888
889    /**
890     * Returns true if the user with the specified identifier is connected to
891     * WordPress.com.
892     *
893     * @param int $user_id the user identifier. Default is the current user.
894     * @return bool Boolean is the user connected?
895     */
896    public function is_user_connected( $user_id = false ) {
897        $user_id = false === $user_id ? get_current_user_id() : absint( $user_id );
898        if ( ! $user_id ) {
899            return false;
900        }
901
902        return (bool) $this->get_tokens()->get_access_token( $user_id );
903    }
904
905    /**
906     * Returns the local user ID of the connection owner.
907     *
908     * @return bool|int Returns the ID of the connection owner or False if no connection owner found.
909     */
910    public function get_connection_owner_id() {
911        // Check if the memoized value is available.
912        if ( null === self::$connection_owner_id ) {
913            $owner                     = $this->get_connection_owner();
914            self::$connection_owner_id = $owner instanceof \WP_User ? $owner->ID : 0;
915        }
916
917        // If the ID is set to 0, there's no valid connection owner.
918        return self::$connection_owner_id > 0 ? self::$connection_owner_id : false;
919    }
920
921    /**
922     * Get the wpcom user data of the current|specified connected user.
923     *
924     * Fetches the data from the WordPress.com `jetpack-wpcom-user-data` REST endpoint
925     * with a signed request as the connected user. Routing this through
926     * Client::remote_request() (rather than the legacy `wpcom.getUser` XML-RPC method)
927     * ensures any connection errors are captured by the Error_Handler.
928     *
929     * @since 8.8.1 Fetch the data over REST instead of the `wpcom.getUser` XML-RPC method.
930     *
931     * @param int|null $user_id the user identifier.
932     * @return bool|array An array with the WPCOM user data on success, false otherwise.
933     */
934    public function get_connected_user_data( $user_id = null ) {
935        if ( ! $user_id ) {
936            $user_id = get_current_user_id();
937        }
938
939        // Check if the user is connected and return false otherwise.
940        if ( ! $this->is_user_connected( $user_id ) ) {
941            return false;
942        }
943
944        $transient_key    = "jetpack_connected_user_data_$user_id";
945        $cached_user_data = get_transient( $transient_key );
946
947        if ( 'error' === $cached_user_data ) {
948            return false;
949        }
950
951        if ( $cached_user_data ) {
952            return $cached_user_data;
953        }
954
955        $blog_id = (int) \Jetpack_Options::get_option( 'id' );
956
957        // Build a signed request as the connected user. We can't use
958        // Client::wpcom_json_api_request_as_user() because it always signs as the
959        // current user, whereas this method may be called for an arbitrary $user_id.
960        $args            = Client::validate_args_for_wpcom_json_api_request(
961            "/sites/{$blog_id}/jetpack-wpcom-user-data",
962            '2',
963            array( 'method' => 'GET' )
964        );
965        $args['user_id'] = $user_id;
966
967        $response = Client::remote_request( $args );
968
969        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
970            // Cache errors briefly so a failing remote request doesn't result in
971            // a blocking request on every call, e.g. on each admin page
972            // load via Initial_State::set_connection_script_data().
973            set_transient( $transient_key, 'error', 5 * MINUTE_IN_SECONDS );
974
975            return false;
976        }
977
978        $user_data = json_decode( wp_remote_retrieve_body( $response ), true );
979
980        if ( ! is_array( $user_data ) || empty( $user_data ) ) {
981            set_transient( $transient_key, 'error', 5 * MINUTE_IN_SECONDS );
982
983            return false;
984        }
985
986        set_transient( $transient_key, $user_data, DAY_IN_SECONDS );
987
988        return $user_data;
989    }
990
991    /**
992     * Fetch the site's own record from the WordPress.com `/sites/%d` endpoint.
993     *
994     * @since 8.10.0
995     *
996     * @return object|WP_Error The decoded site record, or an error describing the failure.
997     */
998    public function get_connected_site_data() {
999        $site_id = \Jetpack_Options::get_option( 'id' );
1000
1001        if ( ! $site_id ) {
1002            return new WP_Error( 'site_id_missing', '', array( 'api_error_code' => 'site_id_missing' ) );
1003        }
1004
1005        $args = array( 'headers' => array() );
1006
1007        // Allow use a store sandbox. Internal ref: PCYsg-IA-p2.
1008        if ( isset( $_COOKIE ) && isset( $_COOKIE['store_sandbox'] ) ) {
1009            // Keep only RFC 6265 cookie-octets so the value cannot break out of the Cookie header.
1010            $secret                    = preg_replace( '/[^\x21-\x7E]|[";,\\\\]/', '', filter_var( wp_unslash( $_COOKIE['store_sandbox'] ) ) );
1011            $args['headers']['Cookie'] = "store_sandbox=$secret;";
1012        }
1013
1014        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', $site_id ) . '?force=wpcom', '1.1', $args );
1015        $body     = wp_remote_retrieve_body( $response );
1016        $data     = $body ? json_decode( $body ) : null;
1017
1018        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1019            $error_info = array(
1020                'api_error_code' => null,
1021                'api_http_code'  => wp_remote_retrieve_response_code( $response ),
1022            );
1023
1024            if ( is_wp_error( $response ) ) {
1025                $error_info['api_error_code'] = $response->get_error_code() ? wp_strip_all_tags( $response->get_error_code() ) : null;
1026            } elseif ( $data && ! empty( $data->error ) ) {
1027                $error_info['api_error_code'] = is_string( $data->error ) ? wp_strip_all_tags( $data->error ) : null;
1028            }
1029
1030            return new WP_Error( 'site_data_fetch_failed', '', $error_info );
1031        }
1032
1033        if ( ! is_object( $data ) ) {
1034            return new WP_Error(
1035                'site_data_fetch_failed',
1036                '',
1037                array(
1038                    'api_error_code' => 'invalid_body',
1039                    'api_http_code'  => 200,
1040                )
1041            );
1042        }
1043
1044        /**
1045         * Fires after the site record was fetched from WordPress.com.
1046         *
1047         * Consumers that cache anything derived from the record, such as the current plan,
1048         * can refresh it here.
1049         *
1050         * The record is passed as an array rather than the object this method returns, so that a
1051         * listener cannot mutate the instance that becomes the REST response.
1052         *
1053         * @since 8.10.0
1054         *
1055         * @param array $record The decoded site record from the WordPress.com `/sites/%d` endpoint.
1056         */
1057        do_action( 'jetpack_site_data_fetched', json_decode( $body, true ) );
1058
1059        return $data;
1060    }
1061
1062    /**
1063     * Returns a user object of the connection owner.
1064     *
1065     * @return WP_User|false False if no connection owner found.
1066     */
1067    public function get_connection_owner() {
1068        $user_id = \Jetpack_Options::get_option( 'master_user' );
1069        if ( ! $user_id ) {
1070            return false;
1071        }
1072
1073        // Make sure user is connected.
1074        $user_token = $this->get_tokens()->get_access_token( $user_id );
1075
1076        $connection_owner = false;
1077
1078        if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) {
1079            $connection_owner = get_userdata( $user_token->external_user_id );
1080        }
1081
1082        // Reporting is best-effort and must never fatal a request running mid-plugin-update:
1083        // skip it when the already-loaded Error_Handler is a stale version predating the factory.
1084        if ( $connection_owner === false && method_exists( Error_Handler::class, 'build_connection_wp_error' ) ) {
1085            Error_Handler::get_instance()->report_error(
1086                Error_Handler::build_connection_wp_error(
1087                    'invalid_connection_owner',
1088                    'Invalid connection owner',
1089                    array( 'token' => '' ),
1090                    'local_state', // Error_Handler::ERROR_TYPE_LOCAL_STATE.
1091                    '', // Local-state errors describe the site's database, not a request, so they have no direction.
1092                    array(
1093                        'user_id'        => $user_id,
1094                        'has_user_token' => (bool) $user_token,
1095                    )
1096                ),
1097                false,
1098                true
1099            );
1100        }
1101
1102        return $connection_owner;
1103    }
1104
1105    /**
1106     * Returns true if the provided user is the Jetpack connection owner.
1107     * If user ID is not specified, the current user will be used.
1108     *
1109     * @param int|bool $user_id the user identifier. False for current user.
1110     * @return bool True the user the connection owner, false otherwise.
1111     */
1112    public function is_connection_owner( $user_id = false ) {
1113        if ( ! $user_id ) {
1114            $user_id = get_current_user_id();
1115        }
1116
1117        return ( (int) $user_id ) === $this->get_connection_owner_id();
1118    }
1119
1120    /**
1121     * Determines whether the connection ownership can be transferred to another user.
1122     *
1123     * The default Jetpack connection uses a transferable ownership model. A consumer
1124     * can declare ownership locked by returning `false` from the `jetpack_connection_ownership_transferable`
1125     * filter. This is the single chokepoint used both when deciding which connection-error
1126     * CTA to surface and (eventually) when performing an ownership change.
1127     *
1128     * @since 8.8.0
1129     *
1130     * @return bool True if ownership can be transferred, false if it is locked.
1131     */
1132    public function is_ownership_transferable() {
1133        /**
1134         * Filters whether the Jetpack connection ownership can be transferred.
1135         *
1136         * Return `false` to lock ownership so it can never be taken over.
1137         *
1138         * @since 8.8.0
1139         *
1140         * @param bool $transferable Whether ownership can be transferred. Default true.
1141         */
1142        return (bool) apply_filters( 'jetpack_connection_ownership_transferable', true );
1143    }
1144
1145    /**
1146     * Connects the user with a specified ID to a WordPress.com user using the
1147     * remote login flow.
1148     *
1149     * @access public
1150     *
1151     * @param int|null    $user_id (optional) the user identifier, defaults to current user.
1152     * @param string|null $redirect_url the URL to redirect the user to for processing, defaults to
1153     *                             admin_url().
1154     * @return WP_Error only in case of a failed user lookup.
1155     */
1156    public function connect_user( $user_id = null, $redirect_url = null ) {
1157        $user = null;
1158        if ( null === $user_id ) {
1159            $user = wp_get_current_user();
1160        } else {
1161            $user = get_user_by( 'ID', $user_id );
1162        }
1163
1164        if ( empty( $user ) ) {
1165            return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' );
1166        }
1167
1168        if ( null === $redirect_url ) {
1169            $redirect_url = admin_url();
1170        }
1171
1172        // Using wp_redirect intentionally because we're redirecting outside.
1173        wp_redirect( $this->get_authorization_url( $user, $redirect_url ) ); // phpcs:ignore WordPress.Security.SafeRedirect
1174        exit( 0 );
1175    }
1176
1177    /**
1178     * Force user disconnect.
1179     *
1180     * @param int  $user_id Local (external) user ID.
1181     * @param bool $disconnect_all_users Whether to disconnect all users before disconnecting the primary user.
1182     *
1183     * @return bool
1184     */
1185    public function disconnect_user_force( $user_id, $disconnect_all_users = false ) {
1186        if ( ! (int) $user_id ) {
1187            // Missing user ID.
1188            return false;
1189        }
1190        // If we are disconnecting the primary user we may need to disconnect all other users first
1191        if ( $user_id === $this->get_connection_owner_id() && $disconnect_all_users && ! $this->disconnect_all_users_except_primary() ) {
1192            return false;
1193        }
1194
1195        return $this->disconnect_user( $user_id, true, true );
1196    }
1197
1198    /**
1199     * Disconnects all users except the primary user.
1200     *
1201     * @return bool
1202     */
1203    public function disconnect_all_users_except_primary() {
1204
1205        $all_connected_users = $this->get_connected_users();
1206
1207        foreach ( $all_connected_users as $user ) {
1208            // Skip the primary.
1209            if ( $user->ID === $this->get_connection_owner_id() ) {
1210                continue;
1211            }
1212            $disconnected = $this->disconnect_user( $user->ID, false, true );
1213            // If we fail to disconnect any user, we should not proceed with disconnecting the primary user.
1214            if ( ! $disconnected ) {
1215                return false;
1216            }
1217        }
1218
1219        return true;
1220    }
1221
1222    /**
1223     * Unlinks the current user from the linked WordPress.com user.
1224     *
1225     * @access public
1226     * @static
1227     *
1228     * @todo Refactor to properly load the XMLRPC client independently.
1229     *
1230     * @param int|null $user_id the user identifier.
1231     * @param bool     $can_overwrite_primary_user Allow for the primary user to be disconnected.
1232     * @param bool     $force_disconnect_locally Disconnect user locally even if we were unable to disconnect them from WP.com.
1233     * @return bool Whether the disconnection of the user was successful.
1234     */
1235    public function disconnect_user( $user_id = null, $can_overwrite_primary_user = false, $force_disconnect_locally = false ) {
1236        $user_id         = empty( $user_id ) ? get_current_user_id() : (int) $user_id;
1237        $is_primary_user = Jetpack_Options::get_option( 'master_user' ) === $user_id;
1238
1239        if ( $is_primary_user && ! $can_overwrite_primary_user ) {
1240            return false;
1241        }
1242
1243        if ( in_array( $user_id, self::$disconnected_users, true ) ) {
1244            // The user is already disconnected.
1245            return false;
1246        }
1247
1248        // Attempt to disconnect the user from WordPress.com.
1249        $is_disconnected_from_wpcom = $this->unlink_user_from_wpcom( $user_id );
1250
1251        $is_disconnected_locally = false;
1252        if ( $is_disconnected_from_wpcom || $force_disconnect_locally ) {
1253            // Get the WordPress.com email before disconnecting the user
1254            $wpcom_user_data = $this->get_connected_user_data( $user_id );
1255            $wpcom_email     = $wpcom_user_data['email'] ?? null;
1256
1257            // Disconnect the user locally.
1258            $is_disconnected_locally = $this->get_tokens()->disconnect_user( $user_id );
1259
1260            if ( $is_disconnected_locally ) {
1261                // Delete cached connected user data.
1262                $transient_key = "jetpack_connected_user_data_$user_id";
1263                delete_transient( $transient_key );
1264
1265                // Clean up account mismatch transients for this user
1266                if ( $wpcom_email ) {
1267                    $user_account_status = new User_Account_Status();
1268                    $user_account_status->clean_account_mismatch_transients( $wpcom_email );
1269                }
1270
1271                /**
1272                 * Fires after the current user has been unlinked from WordPress.com.
1273                 *
1274                 * @since 1.7.0
1275                 * @since-jetpack 4.1.0
1276                 *
1277                 * @param int $user_id The current user's ID.
1278                 */
1279                do_action( 'jetpack_unlinked_user', $user_id );
1280
1281                if ( $is_primary_user ) {
1282                    Jetpack_Options::delete_option( 'master_user' );
1283
1284                    // Clear the memoized connection owner ID since it changed
1285                    self::$connection_owner_id = null;
1286                }
1287            }
1288        }
1289
1290        self::$disconnected_users[] = $user_id;
1291
1292        return $is_disconnected_from_wpcom && $is_disconnected_locally;
1293    }
1294
1295    /**
1296     * Request to wpcom for a user to be unlinked from their WordPress.com account
1297     *
1298     * @param int $user_id The user identifier.
1299     *
1300     * @return bool Whether the disconnection of the user was successful.
1301     */
1302    public function unlink_user_from_wpcom( $user_id ) {
1303        // Attempt to disconnect the user from WordPress.com.
1304        $xml = new Jetpack_IXR_Client();
1305
1306        $xml->query( 'jetpack.unlink_user', $user_id );
1307        if ( $xml->isError() ) {
1308            return false;
1309        }
1310
1311        return (bool) $xml->getResponse();
1312    }
1313
1314    /**
1315     * Update the connection owner.
1316     *
1317     * @since 1.29.0
1318     *
1319     * @param int $new_owner_id The ID of the user to become the connection owner.
1320     *
1321     * @return true|WP_Error True if owner successfully changed, WP_Error otherwise.
1322     */
1323    public function update_connection_owner( $new_owner_id ) {
1324        $roles = new Roles();
1325        if ( ! user_can( $new_owner_id, $roles->translate_role_to_cap( 'administrator' ) ) ) {
1326            return new WP_Error(
1327                'new_owner_not_admin',
1328                __( 'New owner is not admin', 'jetpack-connection' ),
1329                array( 'status' => 400 )
1330            );
1331        }
1332
1333        $old_owner_id = $this->get_connection_owner_id();
1334
1335        if ( $old_owner_id === $new_owner_id ) {
1336            return new WP_Error(
1337                'new_owner_is_existing_owner',
1338                __( 'New owner is same as existing owner', 'jetpack-connection' ),
1339                array( 'status' => 400 )
1340            );
1341        }
1342
1343        if ( ! $this->is_user_connected( $new_owner_id ) ) {
1344            return new WP_Error(
1345                'new_owner_not_connected',
1346                __( 'New owner is not connected', 'jetpack-connection' ),
1347                array( 'status' => 400 )
1348            );
1349        }
1350
1351        // Notify WPCOM about the connection owner change.
1352        $owner_updated_wpcom = $this->update_connection_owner_wpcom( $new_owner_id );
1353
1354        if ( $owner_updated_wpcom ) {
1355            // Update the connection owner in Jetpack only if they were successfully updated on WPCOM.
1356            // This will ensure consistency with WPCOM.
1357            \Jetpack_Options::update_option( 'master_user', $new_owner_id );
1358
1359            // Clear the memoized connection owner ID since it changed
1360            self::$connection_owner_id = null;
1361
1362            // Track it.
1363            ( new Tracking() )->record_user_event( 'set_connection_owner_success' );
1364
1365            return true;
1366        }
1367        return new WP_Error(
1368            'error_setting_new_owner',
1369            __( 'Could not confirm new owner.', 'jetpack-connection' ),
1370            array( 'status' => 500 )
1371        );
1372    }
1373
1374    /**
1375     * Request to WPCOM to update the connection owner.
1376     *
1377     * @since 1.29.0
1378     *
1379     * @param int $new_owner_id The ID of the user to become the connection owner.
1380     *
1381     * @return bool Whether the ownership transfer was successful.
1382     */
1383    public function update_connection_owner_wpcom( $new_owner_id ) {
1384        // Notify WPCOM about the connection owner change.
1385        $xml = new Jetpack_IXR_Client(
1386            array(
1387                'user_id' => get_current_user_id(),
1388            )
1389        );
1390        $xml->query(
1391            'jetpack.switchBlogOwner',
1392            array(
1393                'new_blog_owner' => $new_owner_id,
1394            )
1395        );
1396        if ( $xml->isError() ) {
1397            return false;
1398        }
1399
1400        return (bool) $xml->getResponse();
1401    }
1402
1403    /**
1404     * Returns the requested Jetpack API URL.
1405     *
1406     * @param string $relative_url the relative API path.
1407     * @return string API URL.
1408     */
1409    public function api_url( $relative_url ) {
1410        $api_base    = Constants::get_constant( 'JETPACK__API_BASE' );
1411        $api_version = '/' . Constants::get_constant( 'JETPACK__API_VERSION' ) . '/';
1412
1413        /**
1414         * Filters the API URL that Jetpack uses for server communication.
1415         *
1416         * @since 1.7.0
1417         * @since-jetpack 8.0.0
1418         *
1419         * @param string $url the generated URL.
1420         * @param string $relative_url the relative URL that was passed as an argument.
1421         * @param string $api_base the API base string that is being used.
1422         * @param string $api_version the API version string that is being used.
1423         */
1424        return apply_filters(
1425            'jetpack_api_url',
1426            rtrim( $api_base . $relative_url, '/\\' ) . $api_version,
1427            $relative_url,
1428            $api_base,
1429            $api_version
1430        );
1431    }
1432
1433    /**
1434     * Returns the Jetpack XMLRPC WordPress.com API endpoint URL.
1435     *
1436     * @return string XMLRPC API URL.
1437     */
1438    public function xmlrpc_api_url() {
1439        $base = preg_replace(
1440            '#(https?://[^?/]+)(/?.*)?$#',
1441            '\\1',
1442            Constants::get_constant( 'JETPACK__API_BASE' )
1443        );
1444        return untrailingslashit( $base ) . '/xmlrpc.php';
1445    }
1446
1447    /**
1448     * Attempts Jetpack registration which sets up the site for connection. Should
1449     * remain public because the call to action comes from the current site, not from
1450     * WordPress.com.
1451     *
1452     * @param string $api_endpoint (optional) an API endpoint to use, defaults to 'register'.
1453     * @return true|WP_Error The error object.
1454     */
1455    public function register( $api_endpoint = 'register' ) {
1456        // Clean-up leftover tokens just in-case.
1457        // This fixes an edge case that was preventing users to register when the blog token was missing but
1458        // there were still leftover user tokens present.
1459        $this->delete_all_connection_tokens( true );
1460
1461        add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) );
1462        $secrets = ( new Secrets() )->generate( 'register', get_current_user_id(), 600 );
1463
1464        if ( false === $secrets ) {
1465            return new WP_Error( 'cannot_save_secrets', __( 'Jetpack experienced an issue trying to save options (cannot_save_secrets). We suggest that you contact your hosting provider, and ask them for help checking that the options table is writable on your site.', 'jetpack-connection' ) );
1466        }
1467
1468        if (
1469            empty( $secrets['secret_1'] ) ||
1470            empty( $secrets['secret_2'] ) ||
1471            empty( $secrets['exp'] )
1472        ) {
1473            return new \WP_Error( 'missing_secrets' );
1474        }
1475
1476        // Better to try (and fail) to set a higher timeout than this system
1477        // supports than to have register fail for more users than it should.
1478        $timeout = $this->set_min_time_limit( 60 ) / 2;
1479
1480        $gmt_offset = get_option( 'gmt_offset' );
1481        if ( ! $gmt_offset ) {
1482            $gmt_offset = 0;
1483        }
1484
1485        $stats_options = get_option( 'stats_options' );
1486        $stats_id      = $stats_options['blog_id'] ?? null;
1487
1488        /* This action is documented in src/class-package-version-tracker.php */
1489        $package_versions = apply_filters( 'jetpack_package_versions', array() );
1490
1491        $active_plugins_using_connection = Plugin_Storage::get_all();
1492
1493        /**
1494         * Filters the request body for additional property addition.
1495         *
1496         * @since 1.7.0
1497         * @since-jetpack 7.7.0
1498         *
1499         * @param array $post_data request data.
1500         * @param Array $token_data token data.
1501         */
1502        $body = apply_filters(
1503            'jetpack_register_request_body',
1504            array_merge(
1505                array(
1506                    'siteurl'                  => Urls::site_url(),
1507                    'home'                     => Urls::home_url(),
1508                    'gmt_offset'               => $gmt_offset,
1509                    'timezone_string'          => (string) get_option( 'timezone_string' ),
1510                    'site_name'                => (string) get_option( 'blogname' ),
1511                    'secret_1'                 => $secrets['secret_1'],
1512                    'secret_2'                 => $secrets['secret_2'],
1513                    'site_lang'                => get_locale(),
1514                    'timeout'                  => $timeout,
1515                    'stats_id'                 => $stats_id,
1516                    'state'                    => get_current_user_id(),
1517                    'site_created'             => $this->get_assumed_site_creation_date(),
1518                    'jetpack_version'          => Constants::get_constant( 'JETPACK__VERSION' ),
1519                    'ABSPATH'                  => Constants::get_constant( 'ABSPATH' ),
1520                    'current_user_email'       => wp_get_current_user()->user_email,
1521                    'connect_plugin'           => $this->get_plugin() ? $this->get_plugin()->get_slug() : null,
1522                    'package_versions'         => $package_versions,
1523                    'active_connected_plugins' => $active_plugins_using_connection,
1524                ),
1525                self::$extra_register_params
1526            )
1527        );
1528
1529        $args = array(
1530            'method'  => 'POST',
1531            'body'    => $body,
1532            'headers' => array(
1533                'Accept' => 'application/json',
1534            ),
1535            'timeout' => $timeout,
1536        );
1537
1538        $args['body'] = static::apply_activation_source_to_args( $args['body'] );
1539
1540        // TODO: fix URLs for bad hosts.
1541        $response = Client::_wp_remote_request(
1542            $this->api_url( $api_endpoint ),
1543            $args,
1544            true
1545        );
1546
1547        // Make sure the response is valid and does not contain any Jetpack errors.
1548        $registration_details = $this->validate_remote_register_response( $response );
1549
1550        if ( is_wp_error( $registration_details ) ) {
1551            return $registration_details;
1552        } elseif ( ! $registration_details ) {
1553            return new \WP_Error(
1554                'unknown_error',
1555                'Unknown error registering your Jetpack site.',
1556                wp_remote_retrieve_response_code( $response )
1557            );
1558        }
1559
1560        if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) {
1561            return new \WP_Error(
1562                'jetpack_secret',
1563                'Unable to validate registration of your Jetpack site.',
1564                wp_remote_retrieve_response_code( $response )
1565            );
1566        }
1567
1568        if ( isset( $registration_details->jetpack_public ) ) {
1569            $jetpack_public = (int) $registration_details->jetpack_public;
1570        } else {
1571            $jetpack_public = false;
1572        }
1573
1574        Jetpack_Options::update_options(
1575            array(
1576                'id'     => (int) $registration_details->jetpack_id,
1577                'public' => $jetpack_public,
1578            )
1579        );
1580
1581        update_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION, $package_versions );
1582
1583        $this->get_tokens()->update_blog_token( (string) $registration_details->jetpack_secret );
1584
1585        if ( ! Jetpack_Options::get_option( 'id' ) || ! $this->get_tokens()->get_access_token() ) {
1586            return new WP_Error(
1587                'connection_data_save_failed',
1588                'Failed to save connection data in the database'
1589            );
1590        }
1591
1592        $alternate_authorization_url = $registration_details->alternate_authorization_url ?? '';
1593
1594        add_filter(
1595            'jetpack_register_site_rest_response',
1596            function ( $response ) use ( $alternate_authorization_url ) {
1597                $response['alternateAuthorizeUrl'] = $alternate_authorization_url;
1598                return $response;
1599            }
1600        );
1601
1602        /**
1603         * Fires when a site is registered on WordPress.com.
1604         *
1605         * @since 1.7.0
1606         * @since-jetpack 3.7.0
1607         *
1608         * @param int $json->jetpack_id Jetpack Blog ID.
1609         * @param string $json->jetpack_secret Jetpack Blog Token.
1610         * @param int|bool $jetpack_public Is the site public.
1611         */
1612        do_action(
1613            'jetpack_site_registered',
1614            $registration_details->jetpack_id,
1615            $registration_details->jetpack_secret,
1616            $jetpack_public
1617        );
1618
1619        if ( isset( $registration_details->token ) ) {
1620            /**
1621             * Fires when a user token is sent along with the registration data.
1622             *
1623             * @since 1.7.0
1624             * @since-jetpack 7.6.0
1625             *
1626             * @param object $token the administrator token for the newly registered site.
1627             */
1628            do_action( 'jetpack_site_registered_user_token', $registration_details->token );
1629        }
1630
1631        return true;
1632    }
1633
1634    /**
1635     * Attempts Jetpack registration.
1636     *
1637     * @param bool $tos_agree Whether the user agreed to TOS.
1638     *
1639     * @return bool|WP_Error
1640     */
1641    public function try_registration( $tos_agree = true ) {
1642        if ( $tos_agree ) {
1643            $terms_of_service = new Terms_Of_Service();
1644            $terms_of_service->agree();
1645        }
1646
1647        /**
1648         * Action fired when the user attempts the registration.
1649         *
1650         * @since 1.26.0
1651         */
1652        $pre_register = apply_filters( 'jetpack_pre_register', null );
1653
1654        if ( is_wp_error( $pre_register ) ) {
1655            return $pre_register;
1656        }
1657
1658        $tracking_data = array();
1659
1660        if ( null !== $this->get_plugin() ) {
1661            $tracking_data['plugin_slug'] = $this->get_plugin()->get_slug();
1662        }
1663
1664        $tracking = new Tracking();
1665        $tracking->record_user_event( 'jpc_register_begin', $tracking_data );
1666
1667        add_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) );
1668
1669        $result = $this->register();
1670
1671        remove_filter( 'jetpack_register_request_body', array( Utils::class, 'filter_register_request_body' ) );
1672
1673        // If there was an error with registration and the site was not registered, record this so we can show a message.
1674        if ( ! $result || is_wp_error( $result ) ) {
1675            return $result;
1676        }
1677
1678        return true;
1679    }
1680
1681    /**
1682     * Adds a parameter to the register request body
1683     *
1684     * @since 1.26.0
1685     *
1686     * @param string $name The name of the parameter to be added.
1687     * @param string $value The value of the parameter to be added.
1688     *
1689     * @throws \InvalidArgumentException If supplied arguments are not strings.
1690     * @return void
1691     */
1692    public function add_register_request_param( $name, $value ) {
1693        if ( ! is_string( $name ) || ! is_string( $value ) ) {
1694            throw new \InvalidArgumentException( 'name and value must be strings' );
1695        }
1696        self::$extra_register_params[ $name ] = $value;
1697    }
1698
1699    /**
1700     * Takes the response from the Jetpack register new site endpoint and
1701     * verifies it worked properly.
1702     *
1703     * @since 1.7.0
1704     * @since-jetpack 2.6.0
1705     *
1706     * @param mixed $response the response object, or the error object.
1707     * @return string|WP_Error A JSON object on success or WP_Error on failures
1708     **/
1709    protected function validate_remote_register_response( $response ) {
1710        if ( is_wp_error( $response ) ) {
1711            return new \WP_Error(
1712                'register_http_request_failed',
1713                $response->get_error_message()
1714            );
1715        }
1716
1717        $code   = wp_remote_retrieve_response_code( $response );
1718        $entity = wp_remote_retrieve_body( $response );
1719
1720        if ( $entity ) {
1721            $registration_response = json_decode( $entity );
1722        } else {
1723            $registration_response = false;
1724        }
1725
1726        $code_type = (int) ( $code / 100 );
1727        if ( 5 === $code_type ) {
1728            return new \WP_Error( 'wpcom_5??', $code );
1729        } elseif ( 408 === $code ) {
1730            return new \WP_Error( 'wpcom_408', $code );
1731        } elseif ( ! empty( $registration_response->error ) ) {
1732            if (
1733                'xml_rpc-32700' === $registration_response->error
1734                && ! function_exists( 'xml_parser_create' )
1735            ) {
1736                $error_description = __( "PHP's XML extension is not available. Jetpack requires the XML extension to communicate with WordPress.com. Please contact your hosting provider to enable PHP's XML extension.", 'jetpack-connection' );
1737            } else {
1738                $error_description = isset( $registration_response->error_description )
1739                    ? (string) $registration_response->error_description
1740                    : '';
1741            }
1742
1743            return new \WP_Error(
1744                (string) $registration_response->error,
1745                $error_description,
1746                $code
1747            );
1748        } elseif ( 200 !== $code ) {
1749            return new \WP_Error( 'wpcom_bad_response', $code );
1750        }
1751
1752        // Jetpack ID error block.
1753        if ( empty( $registration_response->jetpack_id ) ) {
1754            return new \WP_Error(
1755                'jetpack_id',
1756                /* translators: %s is an error message string */
1757                sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ),
1758                $entity
1759            );
1760        } elseif ( ! is_scalar( $registration_response->jetpack_id ) ) {
1761            return new \WP_Error(
1762                'jetpack_id',
1763                /* translators: %s is an error message string */
1764                sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ),
1765                $entity
1766            );
1767        } elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) {
1768            return new \WP_Error(
1769                'jetpack_id',
1770                /* translators: %s is an error message string */
1771                sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack-connection' ), $entity ),
1772                $entity
1773            );
1774        }
1775
1776        return $registration_response;
1777    }
1778
1779    /**
1780     * Adds a used nonce to a list of known nonces.
1781     *
1782     * @param int    $timestamp the current request timestamp.
1783     * @param string $nonce the nonce value.
1784     * @return bool whether the nonce is unique or not.
1785     *
1786     * @deprecated since 1.24.0
1787     * @see Nonce_Handler::add()
1788     */
1789    public function add_nonce( $timestamp, $nonce ) {
1790        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::add' );
1791        return ( new Nonce_Handler() )->add( $timestamp, $nonce );
1792    }
1793
1794    /**
1795     * Cleans nonces that were saved when calling ::add_nonce.
1796     *
1797     * @todo Properly prepare the query before executing it.
1798     *
1799     * @param bool $all whether to clean even non-expired nonces.
1800     *
1801     * @deprecated since 1.24.0
1802     * @see Nonce_Handler::clean_all()
1803     */
1804    public function clean_nonces( $all = false ) {
1805        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Nonce_Handler::clean_all' );
1806        ( new Nonce_Handler() )->clean_all( $all ? PHP_INT_MAX : ( time() - Nonce_Handler::LIFETIME ) );
1807    }
1808
1809    /**
1810     * Sets the Connection custom capabilities.
1811     *
1812     * @param string[] $caps    Array of the user's capabilities.
1813     * @param string   $cap     Capability name.
1814     * @param int      $user_id The user ID.
1815     * @param array    $args    Adds the context to the cap. Typically the object ID.
1816     */
1817    public function jetpack_connection_custom_caps( $caps, $cap, $user_id, $args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1818        switch ( $cap ) {
1819            case 'jetpack_connect':
1820            case 'jetpack_reconnect':
1821                $is_offline_mode = ( new Status() )->is_offline_mode();
1822                if ( $is_offline_mode ) {
1823                    $caps = array( 'do_not_allow' );
1824                    break;
1825                }
1826                // Pass through. If it's not offline mode, these should match disconnect.
1827                // Let users disconnect if it's offline mode, just in case things glitch.
1828            case 'jetpack_disconnect':
1829                /**
1830                 * Filters the jetpack_disconnect capability.
1831                 *
1832                 * @since 1.14.2
1833                 *
1834                 * @param array An array containing the capability name.
1835                 */
1836                $caps = apply_filters( 'jetpack_disconnect_cap', array( 'manage_options' ) );
1837                break;
1838            case 'jetpack_connect_user':
1839                $is_offline_mode = ( new Status() )->is_offline_mode();
1840                if ( $is_offline_mode ) {
1841                    $caps = array( 'do_not_allow' );
1842                    break;
1843                }
1844                // With site connections in mind, non-admin users can connect their account only if a connection owner exists.
1845                $caps = $this->has_connected_owner() ? array( 'read' ) : array( 'manage_options' );
1846                break;
1847            case 'jetpack_unlink_user':
1848                $is_offline_mode = ( new Status() )->is_offline_mode();
1849                if ( $is_offline_mode ) {
1850                    $caps = array( 'do_not_allow' );
1851                    break;
1852                }
1853
1854                // Non-admins can always disconnect
1855                $caps = array( 'read' );
1856                break;
1857        }
1858        return $caps;
1859    }
1860
1861    /**
1862     * Builds the timeout limit for queries talking with the wpcom servers.
1863     *
1864     * Based on local php max_execution_time in php.ini
1865     *
1866     * @since 1.7.0
1867     * @since-jetpack 5.4.0
1868     * @return int
1869     **/
1870    public function get_max_execution_time() {
1871        $timeout = (int) ini_get( 'max_execution_time' );
1872
1873        // Ensure exec time set in php.ini.
1874        if ( ! $timeout ) {
1875            $timeout = 30;
1876        }
1877        return $timeout;
1878    }
1879
1880    /**
1881     * Sets a minimum request timeout, and returns the current timeout
1882     *
1883     * @since 1.7.0
1884     * @since-jetpack 5.4.0
1885     * @param int $min_timeout the minimum timeout value.
1886     **/
1887    public function set_min_time_limit( $min_timeout ) {
1888        $timeout = $this->get_max_execution_time();
1889        if ( $timeout < $min_timeout ) {
1890            $timeout = $min_timeout;
1891            set_time_limit( $timeout );
1892        }
1893        return $timeout;
1894    }
1895
1896    /**
1897     * Get our assumed site creation date.
1898     * Calculated based on the earlier date of either:
1899     * - Earliest admin user registration date.
1900     * - Earliest date of post of any post type.
1901     *
1902     * @since 1.7.0
1903     * @since-jetpack 7.2.0
1904     *
1905     * @return string Assumed site creation date and time.
1906     */
1907    public function get_assumed_site_creation_date() {
1908        $cached_date = get_transient( 'jetpack_assumed_site_creation_date' );
1909        if ( ! empty( $cached_date ) ) {
1910            return $cached_date;
1911        }
1912
1913        /**
1914         * We don't use the 'ID' field, but need it to overcome a WP caching bug: https://core.trac.wordpress.org/ticket/62003
1915         *
1916         * @todo Remote the 'ID' field from users fetching when the issue is fixed and Jetpack-supported WP versions move beyond it.
1917         */
1918        $earliest_registered_users  = get_users(
1919            array(
1920                'role'    => 'administrator',
1921                'orderby' => 'user_registered',
1922                'order'   => 'ASC',
1923                'fields'  => array( 'ID', 'user_registered' ),
1924                'number'  => 1,
1925            )
1926        );
1927        $earliest_registration_date = $earliest_registered_users[0]->user_registered;
1928
1929        $earliest_posts = get_posts(
1930            array(
1931                'posts_per_page' => 1,
1932                'post_type'      => 'any',
1933                'post_status'    => 'any',
1934                'orderby'        => 'date',
1935                'order'          => 'ASC',
1936            )
1937        );
1938
1939        // If there are no posts at all, we'll count only on user registration date.
1940        if ( $earliest_posts ) {
1941            $earliest_post_date = $earliest_posts[0]->post_date;
1942        } else {
1943            $earliest_post_date = PHP_INT_MAX;
1944        }
1945
1946        $assumed_date = min( $earliest_registration_date, $earliest_post_date );
1947        set_transient( 'jetpack_assumed_site_creation_date', $assumed_date );
1948
1949        return $assumed_date;
1950    }
1951
1952    /**
1953     * Adds the activation source string as a parameter to passed arguments.
1954     *
1955     * @todo Refactor to use rawurlencode() instead of urlencode().
1956     *
1957     * @param array $args arguments that need to have the source added.
1958     * @return array $amended arguments.
1959     */
1960    public static function apply_activation_source_to_args( $args ) {
1961        $activation_source = get_option( 'jetpack_activation_source' );
1962
1963        if ( ! empty( $activation_source[0] ) ) {
1964            // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1965            $args['_as'] = urlencode( $activation_source[0] );
1966        }
1967
1968        if ( ! empty( $activation_source[1] ) ) {
1969            // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1970            $args['_ak'] = urlencode( $activation_source[1] );
1971        }
1972
1973        return $args;
1974    }
1975
1976    /**
1977     * Generates two secret tokens and the end of life timestamp for them.
1978     *
1979     * @param string   $action  The action name.
1980     * @param int|bool $user_id The user identifier.
1981     * @param int      $exp     Expiration time in seconds.
1982     */
1983    public function generate_secrets( $action, $user_id = false, $exp = 600 ) {
1984        return ( new Secrets() )->generate( $action, $user_id, $exp );
1985    }
1986
1987    /**
1988     * Returns two secret tokens and the end of life timestamp for them.
1989     *
1990     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->get() instead.
1991     *
1992     * @param string $action  The action name.
1993     * @param int    $user_id The user identifier.
1994     * @return string|array an array of secrets or an error string.
1995     */
1996    public function get_secrets( $action, $user_id ) {
1997        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->get' );
1998        return ( new Secrets() )->get( $action, $user_id );
1999    }
2000
2001    /**
2002     * Deletes secret tokens in case they, for example, have expired.
2003     *
2004     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->delete() instead.
2005     *
2006     * @param string $action  The action name.
2007     * @param int    $user_id The user identifier.
2008     */
2009    public function delete_secrets( $action, $user_id ) {
2010        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->delete' );
2011        ( new Secrets() )->delete( $action, $user_id );
2012    }
2013
2014    /**
2015     * Deletes all connection tokens and transients from the local Jetpack site.
2016     * If the plugin object has been provided in the constructor, the function first checks
2017     * whether it's the only active connection.
2018     * If there are any other connections, the function will do nothing and return `false`
2019     * (unless `$ignore_connected_plugins` is set to `true`).
2020     *
2021     * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
2022     *
2023     * @return bool True if disconnected successfully, false otherwise.
2024     */
2025    public function delete_all_connection_tokens( $ignore_connected_plugins = false ) {
2026        // refuse to delete if we're not the last Jetpack plugin installed.
2027        if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) {
2028            return false;
2029        }
2030
2031        /**
2032         * Fires upon the disconnect attempt.
2033         * Return `false` to prevent the disconnect.
2034         *
2035         * @since 1.14.2
2036         */
2037        if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) {
2038            return false;
2039        }
2040
2041        // The protected owner anchor is a local cache of a record WordPress.com owns. Dropping it
2042        // here keeps a disconnected site from carrying a lock that names a user who no longer holds
2043        // a token; the anchor is re-established from WordPress.com when the owner reconnects.
2044        \Jetpack_Options::delete_option(
2045            array(
2046                'master_user',
2047                'protected_owner',
2048                'time_diff',
2049                'fallback_no_verify_ssl_certs',
2050            )
2051        );
2052
2053        // Clear the memoized connection owner ID since it changed
2054        self::$connection_owner_id = null;
2055
2056        ( new Secrets() )->delete_all();
2057        $this->get_tokens()->delete_all();
2058
2059        // Delete cached connected user data.
2060        $transient_key = 'jetpack_connected_user_data_' . get_current_user_id();
2061        delete_transient( $transient_key );
2062
2063        // Delete all XML-RPC errors.
2064        Error_Handler::get_instance()->delete_all_errors();
2065
2066        return true;
2067    }
2068
2069    /**
2070     * Tells WordPress.com to disconnect the site and clear all tokens from cached site.
2071     * If the plugin object has been provided in the constructor, the function first check
2072     * whether it's the only active connection.
2073     * If there are any other connections, the function will do nothing and return `false`
2074     * (unless `$ignore_connected_plugins` is set to `true`).
2075     *
2076     * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
2077     *
2078     * @return bool True if disconnected successfully, false otherwise.
2079     */
2080    public function disconnect_site_wpcom( $ignore_connected_plugins = false ) {
2081        if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) {
2082            return false;
2083        }
2084
2085        if ( ( new Status() )->is_offline_mode() && ! apply_filters( 'jetpack_connection_disconnect_site_wpcom_offline_mode', false ) ) {
2086            // Prevent potential disconnect of the live site by removing WPCOM tokens.
2087            return false;
2088        }
2089
2090        /**
2091         * Fires upon the disconnect attempt.
2092         * Return `false` to prevent the disconnect.
2093         *
2094         * @since 1.14.2
2095         */
2096        if ( ! apply_filters( 'jetpack_connection_disconnect_site_wpcom', true, $this ) ) {
2097            return false;
2098        }
2099
2100        $xml = new Jetpack_IXR_Client();
2101        $xml->query( 'jetpack.deregister', get_current_user_id() );
2102
2103        return true;
2104    }
2105
2106    /**
2107     * Disconnect the plugin and remove the tokens.
2108     * This function will automatically perform "soft" or "hard" disconnect depending on whether other plugins are using the connection.
2109     * This is a proxy method to simplify the Connection package API.
2110     *
2111     * @see Manager::disconnect_site()
2112     *
2113     * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called.
2114     * @param bool    $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
2115     * @return bool
2116     */
2117    public function remove_connection( $disconnect_wpcom = true, $ignore_connected_plugins = false ) {
2118
2119        $this->disconnect_site( $disconnect_wpcom, $ignore_connected_plugins );
2120
2121        return true;
2122    }
2123
2124    /**
2125     * Completely clearing up the connection, and initiating reconnect.
2126     *
2127     * @return true|WP_Error True if reconnected successfully, a `WP_Error` object otherwise.
2128     */
2129    public function reconnect() {
2130        ( new Tracking() )->record_user_event( 'restore_connection_reconnect' );
2131
2132        $this->disconnect_site_wpcom( true );
2133
2134        return $this->register();
2135    }
2136
2137    /**
2138     * Validate the tokens, and refresh the invalid ones.
2139     *
2140     * @return string|bool|WP_Error True if connection restored or string indicating what's to be done next. A `WP_Error` object or false otherwise.
2141     */
2142    public function restore() {
2143        // If this is a site connection we need to trigger a full reconnection as our only secure means of
2144        // communication with WPCOM, aka the blog token, is compromised.
2145        if ( $this->is_site_connection() ) {
2146            return $this->reconnect();
2147        }
2148
2149        $validate_tokens_response = $this->get_tokens()->validate();
2150
2151        // If token validation failed, trigger a full reconnection.
2152        if ( is_array( $validate_tokens_response ) &&
2153            isset( $validate_tokens_response['blog_token']['is_healthy'] ) &&
2154            isset( $validate_tokens_response['user_token']['is_healthy'] ) ) {
2155            $blog_token_healthy = $validate_tokens_response['blog_token']['is_healthy'];
2156            $user_token_healthy = $validate_tokens_response['user_token']['is_healthy'];
2157        } else {
2158            $blog_token_healthy = false;
2159            $user_token_healthy = false;
2160        }
2161
2162        // Tokens are both valid, or both invalid. We can't fix the problem we don't see, so the full reconnection is needed.
2163        if ( $blog_token_healthy === $user_token_healthy ) {
2164            $result = $this->reconnect();
2165            return ( true === $result ) ? 'authorize' : $result;
2166        }
2167
2168        if ( ! $blog_token_healthy ) {
2169            return $this->refresh_blog_token();
2170        }
2171
2172        if ( ! $user_token_healthy ) {
2173            return ( true === $this->refresh_user_token() ) ? 'authorize' : false;
2174        }
2175
2176        return false;
2177    }
2178
2179    /**
2180     * Responds to a WordPress.com call to register the current site.
2181     * Should be changed to protected.
2182     *
2183     * @param array $registration_data Array of [ secret_1, user_id ].
2184     */
2185    public function handle_registration( array $registration_data ) {
2186        list( $registration_secret_1, $registration_user_id ) = $registration_data;
2187        if ( empty( $registration_user_id ) ) {
2188            return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack-connection' ), 400 );
2189        }
2190
2191        return ( new Secrets() )->verify( 'register', $registration_secret_1, (int) $registration_user_id );
2192    }
2193
2194    /**
2195     * Perform the API request to validate the blog and user tokens.
2196     *
2197     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->validate_tokens() instead.
2198     *
2199     * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
2200     *
2201     * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
2202     */
2203    public function validate_tokens( $user_id = null ) {
2204        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->validate' );
2205        return $this->get_tokens()->validate( $user_id );
2206    }
2207
2208    /**
2209     * Verify a Previously Generated Secret.
2210     *
2211     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Secrets->verify() instead.
2212     *
2213     * @param string $action   The type of secret to verify.
2214     * @param string $secret_1 The secret string to compare to what is stored.
2215     * @param int    $user_id  The user ID of the owner of the secret.
2216     * @return \WP_Error|string WP_Error on failure, secret_2 on success.
2217     */
2218    public function verify_secrets( $action, $secret_1, $user_id ) {
2219        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Secrets->verify' );
2220        return ( new Secrets() )->verify( $action, $secret_1, $user_id );
2221    }
2222
2223    /**
2224     * Responds to a WordPress.com call to authorize the current user.
2225     * Should be changed to protected.
2226     */
2227    public function handle_authorization() {
2228    }
2229
2230    /**
2231     * Obtains the auth token.
2232     *
2233     * @param array $data The request data.
2234     * @return object|\WP_Error Returns the auth token on success.
2235     *                          Returns a \WP_Error on failure.
2236     */
2237    public function get_token( $data ) {
2238        return $this->get_tokens()->get( $data, $this->api_url( 'token' ) );
2239    }
2240
2241    /**
2242     * Builds a URL to the Jetpack connection auth page.
2243     *
2244     * @since 2.7.6 Added optional $from and $raw parameters.
2245     *
2246     * @param WP_User|null $user     (optional) defaults to the current logged in user.
2247     * @param string|null  $redirect (optional) a redirect URL to use instead of the default.
2248     * @param bool|string  $from     If not false, adds 'from=$from' param to the connect URL.
2249     * @param bool         $raw If true, URL will not be escaped.
2250     *
2251     * @return string Connect URL.
2252     */
2253    public function get_authorization_url( $user = null, $redirect = null, $from = false, $raw = false ) {
2254        if ( empty( $user ) ) {
2255            $user = wp_get_current_user();
2256        }
2257
2258        $roles       = new Roles();
2259        $role        = $roles->translate_user_to_role( $user );
2260        $signed_role = $this->get_tokens()->sign_role( $role );
2261
2262        /**
2263         * Filter the URL of the first time the user gets redirected back to your site for connection
2264         * data processing.
2265         *
2266         * @since 1.7.0
2267         * @since-jetpack 8.0.0
2268         *
2269         * @param string $redirect_url Defaults to the site admin URL.
2270         */
2271        $processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) );
2272
2273        /**
2274         * Filter the URL to redirect the user back to when the authorization process
2275         * is complete.
2276         *
2277         * @since 1.7.0
2278         * @since-jetpack 8.0.0
2279         *
2280         * @param string $redirect_url Defaults to the site URL.
2281         */
2282        $redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect );
2283
2284        $secrets = ( new Secrets() )->generate( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS );
2285
2286        /**
2287         * Filter the type of authorization.
2288         * 'calypso' completes authorization on wordpress.com/jetpack/connect
2289         * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com.
2290         *
2291         * @since 1.7.0
2292         * @since-jetpack 4.3.3
2293         *
2294         * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'.
2295         */
2296        $auth_type = apply_filters( 'jetpack_auth_type', 'calypso' );
2297
2298        $body_args = array(
2299            'response_type'         => 'code',
2300            'client_id'             => \Jetpack_Options::get_option( 'id' ),
2301            'redirect_uri'          => add_query_arg(
2302                array(
2303                    'handler'  => 'jetpack-connection-webhooks',
2304                    'action'   => 'authorize',
2305                    '_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
2306                    'redirect' => $redirect ? rawurlencode( $redirect ) : false,
2307                ),
2308                esc_url( $processing_url )
2309            ),
2310            'state'                 => $user->ID,
2311            'scope'                 => $signed_role,
2312            'user_email'            => $user->user_email,
2313            'user_login'            => $user->user_login,
2314            'is_active'             => $this->has_connected_owner(), // TODO Deprecate this.
2315            'jp_version'            => (string) Constants::get_constant( 'JETPACK__VERSION' ),
2316            'auth_type'             => $auth_type,
2317            'secret'                => $secrets['secret_1'],
2318            'blogname'              => get_option( 'blogname' ),
2319            'site_url'              => Urls::site_url(),
2320            'home_url'              => Urls::home_url(),
2321            'site_icon'             => get_site_icon_url(),
2322            'site_lang'             => get_locale(),
2323            'site_created'          => $this->get_assumed_site_creation_date(),
2324            'allow_site_connection' => ! $this->has_connected_owner(),
2325            'calypso_env'           => ( new Host() )->get_calypso_env(),
2326            'source'                => ( new Host() )->get_source_query(),
2327        );
2328
2329        // Include the slugs of every plugin currently using the Jetpack connection so wpcom
2330        // knows which integrations the site is authorizing on behalf of. `Plugin_Storage::get_all()`
2331        // returns a `WP_Error` when called before `plugins_loaded`; in that case we silently skip.
2332        $active_plugins = Plugin_Storage::get_all();
2333        if ( is_array( $active_plugins ) && ! empty( $active_plugins ) ) {
2334            $body_args['plugins'] = implode( ',', array_keys( $active_plugins ) );
2335        }
2336
2337        // Signal to Calypso that the site already has a connection owner so the
2338        // authorize page can show secondary-connection content where appropriate.
2339        if ( $this->has_connected_owner() ) {
2340            $body_args['has_connected_owner'] = true;
2341        }
2342
2343        /**
2344         * Filters the user connection request data for additional property addition.
2345         *
2346         * @since 1.7.0
2347         * @since-jetpack 8.0.0
2348         *
2349         * @param array $request_data request data.
2350         */
2351        $body = apply_filters( 'jetpack_connect_request_body', $body_args );
2352
2353        $body = static::apply_activation_source_to_args( urlencode_deep( $body ) );
2354
2355        $api_url = $this->api_url( 'authorize' );
2356
2357        $url = add_query_arg( $body, $api_url );
2358
2359        if ( is_network_admin() ) {
2360            $url = add_query_arg( 'is_multisite', network_admin_url( 'admin.php?page=jetpack-settings' ), $url );
2361        }
2362
2363        if ( $from ) {
2364            $url = add_query_arg( 'from', $from, $url );
2365        }
2366
2367        if ( $raw ) {
2368            $url = esc_url_raw( $url );
2369        }
2370
2371        /**
2372         * Filter the URL used when connecting a user to a WordPress.com account.
2373         *
2374         * @since 2.0.0
2375         * @since 2.7.6 Added $raw parameter.
2376         *
2377         * @param string $url Connection URL.
2378         * @param bool   $raw If true, URL will not be escaped.
2379         */
2380        return apply_filters( 'jetpack_build_authorize_url', $url, $raw );
2381    }
2382
2383    /**
2384     * Authorizes the user by obtaining and storing the user token.
2385     *
2386     * @param array $data The request data.
2387     * @return string|\WP_Error Returns a string on success.
2388     *                          Returns a \WP_Error on failure.
2389     */
2390    public function authorize( $data = array() ) {
2391        /**
2392         * Action fired when user authorization starts.
2393         *
2394         * @since 1.7.0
2395         * @since-jetpack 8.0.0
2396         */
2397        do_action( 'jetpack_authorize_starting' );
2398
2399        $roles = new Roles();
2400        $role  = $roles->translate_current_user_to_role();
2401
2402        if ( ! $role ) {
2403            return new \WP_Error( 'no_role', 'Invalid request.', 400 );
2404        }
2405
2406        $cap = $roles->translate_role_to_cap( $role );
2407        if ( ! $cap ) {
2408            return new \WP_Error( 'no_cap', 'Invalid request.', 400 );
2409        }
2410
2411        if ( ! empty( $data['error'] ) ) {
2412            return new \WP_Error( $data['error'], 'Error included in the request.', 400 );
2413        }
2414
2415        if ( ! isset( $data['state'] ) ) {
2416            return new \WP_Error( 'no_state', 'Request must include state.', 400 );
2417        }
2418
2419        if ( ! ctype_digit( $data['state'] ) ) {
2420            return new \WP_Error( $data['error'], 'State must be an integer.', 400 );
2421        }
2422
2423        $current_user_id = get_current_user_id();
2424        if ( $current_user_id !== (int) $data['state'] ) {
2425            return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 );
2426        }
2427
2428        if ( empty( $data['code'] ) ) {
2429            return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 );
2430        }
2431
2432        $token = $this->get_tokens()->get( $data, $this->api_url( 'token' ) );
2433
2434        if ( is_wp_error( $token ) ) {
2435            $code = $token->get_error_code();
2436            if ( empty( $code ) ) {
2437                $code = 'invalid_token';
2438            }
2439            return new \WP_Error( $code, $token->get_error_message(), 400 );
2440        }
2441
2442        if ( ! $token ) {
2443            return new \WP_Error( 'no_token', 'Error generating token.', 400 );
2444        }
2445
2446        $is_connection_owner = ! $this->has_connected_owner();
2447
2448        $this->get_tokens()->update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_connection_owner );
2449
2450        // Delete cached connected user data, so a cached failure from the
2451        // previous (broken) token doesn't linger after reconnecting.
2452        delete_transient( "jetpack_connected_user_data_$current_user_id" );
2453
2454        /**
2455         * Fires after user has successfully received an auth token.
2456         *
2457         * @since 1.7.0
2458         * @since-jetpack 3.9.0
2459         */
2460        do_action( 'jetpack_user_authorized' );
2461
2462        if ( ! $is_connection_owner ) {
2463            /**
2464             * Action fired when a secondary user has been authorized.
2465             *
2466             * @since 1.7.0
2467             * @since-jetpack 8.0.0
2468             */
2469            do_action( 'jetpack_authorize_ending_linked' );
2470            return 'linked';
2471        }
2472
2473        /**
2474         * Action fired when the master user has been authorized.
2475         *
2476         * @since 1.7.0
2477         * @since-jetpack 8.0.0
2478         *
2479         * @param array $data The request data.
2480         */
2481        do_action( 'jetpack_authorize_ending_authorized', $data );
2482
2483        \Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' );
2484
2485        ( new Nonce_Handler() )->reschedule();
2486
2487        return 'authorized';
2488    }
2489
2490    /**
2491     * Disconnects from the Jetpack servers.
2492     * Forgets all connection details and tells the Jetpack servers to do the same.
2493     *
2494     * @param boolean $disconnect_wpcom Should disconnect_site_wpcom be called.
2495     * @param bool    $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
2496     */
2497    public function disconnect_site( $disconnect_wpcom = true, $ignore_connected_plugins = true ) {
2498        if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) {
2499            return false;
2500        }
2501
2502        wp_clear_scheduled_hook( 'jetpack_clean_nonces' );
2503
2504        ( new Nonce_Handler() )->clean_all();
2505
2506        Heartbeat::init()->deactivate();
2507
2508        /**
2509         * Fires before a site is disconnected.
2510         *
2511         * @since 1.36.3
2512         */
2513        do_action( 'jetpack_site_before_disconnected' );
2514
2515        // If the site is in an IDC because sync is not allowed,
2516        // let's make sure to not disconnect the production site.
2517        if ( $disconnect_wpcom ) {
2518            $tracking = new Tracking();
2519            $tracking->record_user_event( 'disconnect_site', array() );
2520
2521            $this->disconnect_site_wpcom( $ignore_connected_plugins );
2522        }
2523
2524        $this->delete_all_connection_tokens( $ignore_connected_plugins );
2525
2526        // Remove tracked package versions, since they depend on the Jetpack Connection.
2527        delete_option( Package_Version_Tracker::PACKAGE_VERSION_OPTION );
2528
2529        $jetpack_unique_connection = \Jetpack_Options::get_option( 'unique_connection' );
2530        if ( $jetpack_unique_connection ) {
2531            // Check then record unique disconnection if site has never been disconnected previously.
2532            if ( - 1 === $jetpack_unique_connection['disconnected'] ) {
2533                $jetpack_unique_connection['disconnected'] = 1;
2534            } else {
2535                if ( 0 === $jetpack_unique_connection['disconnected'] ) {
2536                    $a8c_mc_stats_instance = new A8c_Mc_Stats();
2537                    $a8c_mc_stats_instance->add( 'connections', 'unique-disconnect' );
2538                    $a8c_mc_stats_instance->do_server_side_stats();
2539                }
2540                // increment number of times disconnected.
2541                $jetpack_unique_connection['disconnected'] += 1;
2542            }
2543
2544            \Jetpack_Options::update_option( 'unique_connection', $jetpack_unique_connection );
2545        }
2546
2547        /**
2548         * Fires when a site is disconnected.
2549         *
2550         * @since 1.30.1
2551         */
2552        do_action( 'jetpack_site_disconnected' );
2553    }
2554
2555    /**
2556     * The Base64 Encoding of the SHA1 Hash of the Input.
2557     *
2558     * @param string $text The string to hash.
2559     * @return string
2560     */
2561    public function sha1_base64( $text ) {
2562        return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
2563    }
2564
2565    /**
2566     * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
2567     *
2568     * @param string $domain The domain to check.
2569     *
2570     * @return bool|WP_Error
2571     */
2572    public function is_usable_domain( $domain ) {
2573
2574        // If it's empty, just fail out.
2575        if ( ! $domain ) {
2576            return new \WP_Error(
2577                'fail_domain_empty',
2578                /* translators: %1$s is a domain name. */
2579                sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack-connection' ), $domain )
2580            );
2581        }
2582
2583        /**
2584         * Skips the usuable domain check when connecting a site.
2585         *
2586         * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
2587         *
2588         * @since 1.7.0
2589         * @since-jetpack 4.1.0
2590         *
2591         * @param bool If the check should be skipped. Default false.
2592         */
2593        if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
2594            return true;
2595        }
2596
2597        // None of the explicit localhosts.
2598        $forbidden_domains = array(
2599            'wordpress.com',
2600            'localhost',
2601            'localhost.localdomain',
2602            'local.wordpress.test',         // VVV pattern.
2603            'local.wordpress-trunk.test',   // VVV pattern.
2604            'src.wordpress-develop.test',   // VVV pattern.
2605            'build.wordpress-develop.test', // VVV pattern.
2606        );
2607        if ( in_array( $domain, $forbidden_domains, true ) ) {
2608            return new \WP_Error(
2609                'fail_domain_forbidden',
2610                sprintf(
2611                    /* translators: %1$s is a domain name. */
2612                    __(
2613                        'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
2614                        'jetpack-connection'
2615                    ),
2616                    $domain
2617                )
2618            );
2619        }
2620
2621        // No .test or .local domains.
2622        if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
2623            return new \WP_Error(
2624                'fail_domain_tld',
2625                sprintf(
2626                    /* translators: %1$s is a domain name. */
2627                    __(
2628                        'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
2629                        'jetpack-connection'
2630                    ),
2631                    $domain
2632                )
2633            );
2634        }
2635
2636        // No WPCOM subdomains.
2637        if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
2638            return new \WP_Error(
2639                'fail_subdomain_wpcom',
2640                sprintf(
2641                    /* translators: %1$s is a domain name. */
2642                    __(
2643                        'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
2644                        'jetpack-connection'
2645                    ),
2646                    $domain
2647                )
2648            );
2649        }
2650
2651        // If PHP was compiled without support for the Filter module (very edge case).
2652        if ( ! function_exists( 'filter_var' ) ) {
2653            // Just pass back true for now, and let wpcom sort it out.
2654            return true;
2655        }
2656
2657        $domain = preg_replace( '#^https?://#', '', untrailingslashit( $domain ) );
2658
2659        if ( filter_var( $domain, FILTER_VALIDATE_IP )
2660            && ! \Automattic\Jetpack\IP\Utils::ip_is_public( $domain )
2661        ) {
2662            return new \WP_Error(
2663                'fail_ip_forbidden',
2664                sprintf(
2665                    /* translators: %1$s is a domain name. */
2666                    __(
2667                        'IP address `%1$s` just failed is_usable_domain check as it is not a public IP address.',
2668                        'jetpack-connection'
2669                    ),
2670                    $domain
2671                )
2672            );
2673        }
2674
2675        return true;
2676    }
2677
2678    /**
2679     * Gets the requested token.
2680     *
2681     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_access_token() instead.
2682     *
2683     * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
2684     * @param string|false $token_key If provided, check that the token matches the provided input.
2685     * @param bool|true    $suppress_errors If true, return a falsy value when the token isn't found; When false, return a descriptive WP_Error when the token isn't found.
2686     *
2687     * @return object|false
2688     *
2689     * @see $this->get_tokens()->get_access_token()
2690     */
2691    public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
2692        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_access_token' );
2693        return $this->get_tokens()->get_access_token( $user_id, $token_key, $suppress_errors );
2694    }
2695
2696    /**
2697     * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths
2698     * since it is passed by reference to various methods.
2699     * Capture it here so we can verify the signature later.
2700     *
2701     * @param array $methods an array of available XMLRPC methods.
2702     * @return array the same array, since this method doesn't add or remove anything.
2703     */
2704    public function xmlrpc_methods( $methods ) {
2705        $this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA'] ?? null;
2706        return $methods;
2707    }
2708
2709    /**
2710     * Resets the raw post data parameter for testing purposes.
2711     */
2712    public function reset_raw_post_data() {
2713        $this->raw_post_data = null;
2714    }
2715
2716    /**
2717     * Registering an additional method.
2718     *
2719     * @param array $methods an array of available XMLRPC methods.
2720     * @return array the amended array in case the method is added.
2721     */
2722    public function public_xmlrpc_methods( $methods ) {
2723        if ( array_key_exists( 'wp.getOptions', $methods ) ) {
2724            $methods['wp.getOptions'] = array( $this, 'jetpack_get_options' );
2725        }
2726        return $methods;
2727    }
2728
2729    /**
2730     * Handles a getOptions XMLRPC method call.
2731     *
2732     * @param array $args method call arguments.
2733     * @return array|IXR_Error An amended XMLRPC server options array.
2734     */
2735    public function jetpack_get_options( $args ) {
2736        global $wp_xmlrpc_server;
2737
2738        $wp_xmlrpc_server->escape( $args );
2739
2740        $username = $args[1];
2741        $password = $args[2];
2742
2743        $user = $wp_xmlrpc_server->login( $username, $password );
2744        if ( ! $user ) {
2745            return $wp_xmlrpc_server->error;
2746        }
2747
2748        $options   = array();
2749        $user_data = $this->get_connected_user_data();
2750        if ( is_array( $user_data ) ) {
2751            $options['jetpack_user_id']         = array(
2752                'desc'     => __( 'The WP.com user ID of the connected user', 'jetpack-connection' ),
2753                'readonly' => true,
2754                'value'    => $user_data['ID'],
2755            );
2756            $options['jetpack_user_login']      = array(
2757                'desc'     => __( 'The WP.com username of the connected user', 'jetpack-connection' ),
2758                'readonly' => true,
2759                'value'    => $user_data['login'],
2760            );
2761            $options['jetpack_user_email']      = array(
2762                'desc'     => __( 'The WP.com user email of the connected user', 'jetpack-connection' ),
2763                'readonly' => true,
2764                'value'    => $user_data['email'],
2765            );
2766            $options['jetpack_user_site_count'] = array(
2767                'desc'     => __( 'The number of sites of the connected WP.com user', 'jetpack-connection' ),
2768                'readonly' => true,
2769                'value'    => $user_data['site_count'],
2770            );
2771        }
2772        $wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options );
2773        $args                           = stripslashes_deep( $args );
2774        return $wp_xmlrpc_server->wp_getOptions( $args );
2775    }
2776
2777    /**
2778     * Adds Jetpack-specific options to the output of the XMLRPC options method.
2779     *
2780     * @param array $options standard Core options.
2781     * @return array amended options.
2782     */
2783    public function xmlrpc_options( $options ) {
2784        $jetpack_client_id = false;
2785        if ( $this->is_connected() ) {
2786            $jetpack_client_id = \Jetpack_Options::get_option( 'id' );
2787        }
2788        $options['jetpack_version'] = array(
2789            'desc'     => __( 'Jetpack Plugin Version', 'jetpack-connection' ),
2790            'readonly' => true,
2791            'value'    => Constants::get_constant( 'JETPACK__VERSION' ),
2792        );
2793
2794        $options['jetpack_client_id'] = array(
2795            'desc'     => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack-connection' ),
2796            'readonly' => true,
2797            'value'    => $jetpack_client_id,
2798        );
2799        return $options;
2800    }
2801
2802    /**
2803     * Resets the saved authentication state in between testing requests.
2804     */
2805    public function reset_saved_auth_state() {
2806        $this->xmlrpc_verification = null;
2807    }
2808
2809    /**
2810     * Sign a user role with the master access token.
2811     * If not specified, will default to the current user.
2812     *
2813     * @access public
2814     *
2815     * @param string $role    User role.
2816     * @param int    $user_id ID of the user.
2817     * @return string Signed user role.
2818     */
2819    public function sign_role( $role, $user_id = null ) {
2820        return $this->get_tokens()->sign_role( $role, $user_id );
2821    }
2822
2823    /**
2824     * Set the plugin instance.
2825     *
2826     * @param Plugin $plugin_instance The plugin instance.
2827     *
2828     * @return $this
2829     */
2830    public function set_plugin_instance( Plugin $plugin_instance ) {
2831        $this->plugin = $plugin_instance;
2832
2833        return $this;
2834    }
2835
2836    /**
2837     * Retrieve the plugin management object.
2838     *
2839     * @return Plugin|null
2840     */
2841    public function get_plugin() {
2842        return $this->plugin;
2843    }
2844
2845    /**
2846     * Get all connected plugins information, excluding those disconnected by user.
2847     * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
2848     * Even if you don't use Jetpack Config, it may be introduced later by other plugins,
2849     * so please make sure not to run the method too early in the code.
2850     *
2851     * @return array|WP_Error
2852     */
2853    public function get_connected_plugins() {
2854        $maybe_plugins = Plugin_Storage::get_all();
2855
2856        if ( $maybe_plugins instanceof WP_Error ) {
2857            return $maybe_plugins;
2858        }
2859
2860        return $maybe_plugins;
2861    }
2862
2863    /**
2864     * Force plugin disconnect. After its called, the plugin will not be allowed to use the connection.
2865     * Note: this method does not remove any access tokens.
2866     *
2867     * @deprecated since 1.39.0
2868     * @return bool
2869     */
2870    public function disable_plugin() {
2871        return null;
2872    }
2873
2874    /**
2875     * Force plugin reconnect after user-initiated disconnect.
2876     * After its called, the plugin will be allowed to use the connection again.
2877     * Note: this method does not initialize access tokens.
2878     *
2879     * @deprecated since 1.39.0.
2880     * @return bool
2881     */
2882    public function enable_plugin() {
2883        return null;
2884    }
2885
2886    /**
2887     * Whether the plugin is allowed to use the connection, or it's been disconnected by user.
2888     * If no plugin slug was passed into the constructor, always returns true.
2889     *
2890     * @deprecated 1.42.0 This method no longer has a purpose after the removal of the soft disconnect feature.
2891     *
2892     * @return bool
2893     */
2894    public function is_plugin_enabled() {
2895        return true;
2896    }
2897
2898    /**
2899     * Perform the API request to refresh the blog token.
2900     * Note that we are making this request on behalf of the Jetpack master user,
2901     * given they were (most probably) the ones that registered the site at the first place.
2902     *
2903     * @return WP_Error|bool The result of updating the blog_token option.
2904     */
2905    public function refresh_blog_token() {
2906        ( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' );
2907
2908        $blog_id = \Jetpack_Options::get_option( 'id' );
2909        if ( ! $blog_id ) {
2910            return new WP_Error( 'site_not_registered', 'Site not registered.' );
2911        }
2912
2913        $url     = sprintf(
2914            '%s/%s/v%s/%s',
2915            Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
2916            'wpcom',
2917            '2',
2918            'sites/' . $blog_id . '/jetpack-refresh-blog-token'
2919        );
2920        $method  = 'POST';
2921        $user_id = get_current_user_id();
2922
2923        $response = Client::remote_request( compact( 'url', 'method', 'user_id' ) );
2924
2925        if ( is_wp_error( $response ) ) {
2926            return new WP_Error( 'refresh_blog_token_http_request_failed', $response->get_error_message() );
2927        }
2928
2929        $code   = wp_remote_retrieve_response_code( $response );
2930        $entity = wp_remote_retrieve_body( $response );
2931
2932        if ( $entity ) {
2933            $json = json_decode( $entity );
2934        } else {
2935            $json = false;
2936        }
2937
2938        if ( 200 !== $code ) {
2939            if ( empty( $json->code ) ) {
2940                return new WP_Error( 'unknown', '', $code );
2941            }
2942
2943            /* translators: Error description string. */
2944            $error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack-connection' ), (string) $json->message ) : '';
2945
2946            return new WP_Error( (string) $json->code, $error_description, $code );
2947        }
2948
2949        if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) {
2950            return new WP_Error( 'jetpack_secret', '', $code );
2951        }
2952
2953        Error_Handler::get_instance()->delete_all_errors();
2954
2955        return $this->get_tokens()->update_blog_token( (string) $json->jetpack_secret );
2956    }
2957
2958    /**
2959     * Disconnect the user from WP.com, and initiate the reconnect process.
2960     *
2961     * @return bool
2962     */
2963    public function refresh_user_token() {
2964        ( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' );
2965        $this->disconnect_user( null, true, true );
2966        return true;
2967    }
2968
2969    /**
2970     * Fetches a signed token.
2971     *
2972     * @deprecated 1.24.0 Use Automattic\Jetpack\Connection\Tokens->get_signed_token() instead.
2973     *
2974     * @param object $token the token.
2975     * @return WP_Error|string a signed token
2976     */
2977    public function get_signed_token( $token ) {
2978        _deprecated_function( __METHOD__, '1.24.0', 'Automattic\\Jetpack\\Connection\\Tokens->get_signed_token' );
2979        return $this->get_tokens()->get_signed_token( $token );
2980    }
2981
2982    /**
2983     * If the site-level connection is active, add the list of plugins using connection to the heartbeat (except Jetpack itself)
2984     *
2985     * @since 6.11.0 Add the list of Jetpack package versions to the heartbeat.
2986     * @since 8.7.4 Add the missing connection owner and XML-RPC error stats to the heartbeat.
2987     * @since 8.7.9 Add the site environment stats (WordPress/PHP versions, etc.) to the heartbeat.
2988     *
2989     * @param array $stats The Heartbeat stats array.
2990     * @return array $stats
2991     */
2992    public function add_stats_to_heartbeat( $stats ) {
2993
2994        if ( ! $this->is_connected() ) {
2995            return $stats;
2996        }
2997
2998        $active_plugins_using_connection = Plugin_Storage::get_all();
2999        foreach ( array_keys( $active_plugins_using_connection ) as $plugin_slug ) {
3000            if ( 'jetpack' !== $plugin_slug ) {
3001                $stats_group             = isset( $active_plugins_using_connection['jetpack'] ) ? 'combined-connection' : 'standalone-connection';
3002                $stats[ $stats_group ][] = $plugin_slug;
3003            }
3004        }
3005
3006        $stats['jetpack_package_versions'] = apply_filters( 'jetpack_package_versions', array() );
3007
3008        $stats['identitycrisis'] = Identity_Crisis::check_identity_crisis() ? 'yes' : 'no';
3009
3010        // Missing the connection owner?
3011        $stats['missing-owner'] = $this->is_missing_connection_owner();
3012
3013        $xmlrpc_errors = \Jetpack_Options::get_option( 'xmlrpc_errors', array() );
3014        if ( $xmlrpc_errors ) {
3015            $stats['xmlrpc-errors'] = implode( ',', array_keys( $xmlrpc_errors ) );
3016            \Jetpack_Options::delete_option( 'xmlrpc_errors' );
3017        }
3018
3019        // Site environment stats (WordPress/PHP versions, site configuration, etc.).
3020        $stats = array_merge( $stats, Heartbeat::get_environment_stats() );
3021
3022        return $stats;
3023    }
3024
3025    /**
3026     * Records a failed XML-RPC signature verification so it can be reported in the heartbeat.
3027     *
3028     * We don't want to expose a detailed error message about why a request failed
3029     * signature verification, as doing so could leak information. Instead, we track
3030     * that the error occurred via a Jetpack option and send that data back in the
3031     * heartbeat. All this does is record the error code, but it's enough to find trends.
3032     *
3033     * @since 8.7.4
3034     *
3035     * @param \WP_Error $xmlrpc_error The error produced during signature validation.
3036     * @return void
3037     */
3038    public function track_xmlrpc_error( $xmlrpc_error ) {
3039        $code = is_wp_error( $xmlrpc_error )
3040            ? $xmlrpc_error->get_error_code()
3041            : 'should-not-happen';
3042
3043        $xmlrpc_errors = \Jetpack_Options::get_option( 'xmlrpc_errors', array() );
3044        if ( isset( $xmlrpc_errors[ $code ] ) && $xmlrpc_errors[ $code ] ) {
3045            // No need to update the option if we already have this code stored.
3046            return;
3047        }
3048        $xmlrpc_errors[ $code ] = true;
3049
3050        \Jetpack_Options::update_option( 'xmlrpc_errors', $xmlrpc_errors, false );
3051    }
3052
3053    /**
3054     * Get the WPCOM or self-hosted site ID.
3055     *
3056     * @param bool $quiet Return null instead of an error.
3057     *
3058     * @return int|WP_Error|null
3059     */
3060    public static function get_site_id( $quiet = false ) {
3061        $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
3062        $site_id  = $is_wpcom ? get_current_blog_id() : \Jetpack_Options::get_option( 'id' );
3063        if ( ! $site_id ) {
3064            return $quiet
3065                ? null
3066                : new \WP_Error(
3067                    'unavailable_site_id',
3068                    __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack-connection' ),
3069                    403
3070                );
3071        }
3072        return (int) $site_id;
3073    }
3074
3075    /**
3076     * Check if Jetpack is ready for uninstall cleanup.
3077     *
3078     * @param string $current_plugin_slug The current plugin's slug.
3079     *
3080     * @return bool
3081     */
3082    public static function is_ready_for_cleanup( $current_plugin_slug ) {
3083        $active_plugins = get_option( Plugin_Storage::ACTIVE_PLUGINS_OPTION_NAME );
3084
3085        return empty( $active_plugins ) || ! is_array( $active_plugins )
3086            || ( count( $active_plugins ) === 1 && array_key_exists( $current_plugin_slug, $active_plugins ) );
3087    }
3088}