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