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