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