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