Completed
Push — update/phpunit-php-8-packages ( 8ca541...77692e )
by
unknown
21:50 queued 10:30
created

packages/connection/src/class-manager.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * The Jetpack Connection manager class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
use Automattic\Jetpack\Constants;
11
use Automattic\Jetpack\Heartbeat;
12
use Automattic\Jetpack\Roles;
13
use Automattic\Jetpack\Status;
14
use Automattic\Jetpack\Tracking;
15
use Jetpack_Options;
16
use WP_Error;
17
use WP_User;
18
19
/**
20
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
21
 * and Jetpack.
22
 */
23
class Manager {
24
25
	const SECRETS_MISSING        = 'secrets_missing';
26
	const SECRETS_EXPIRED        = 'secrets_expired';
27
	const SECRETS_OPTION_NAME    = 'jetpack_secrets';
28
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
29
30
	/**
31
	 * Constant used to fetch the master user token. Deprecated.
32
	 *
33
	 * @deprecated 9.0.0
34
	 * @see Manager::CONNECTION_OWNER
35
	 * @var boolean
36
	 */
37
	const JETPACK_MASTER_USER = true; //phpcs:ignore Jetpack.Constants.MasterUserConstant.ShouldNotBeUsed
38
39
	/**
40
	 * For internal use only. If you need to get the connection owner, use the provided methods
41
	 * get_connection_owner_id, get_connection_owner and is_connection_owner
42
	 *
43
	 * @todo Add private visibility once PHP 7.1 is the minimum supported verion.
44
	 *
45
	 * @var boolean
46
	 */
47
	const CONNECTION_OWNER = true;
48
49
	/**
50
	 * The procedure that should be run to generate secrets.
51
	 *
52
	 * @var Callable
53
	 */
54
	protected $secret_callable;
55
56
	/**
57
	 * A copy of the raw POST data for signature verification purposes.
58
	 *
59
	 * @var String
60
	 */
61
	protected $raw_post_data;
62
63
	/**
64
	 * Verification data needs to be stored to properly verify everything.
65
	 *
66
	 * @var Object
67
	 */
68
	private $xmlrpc_verification = null;
69
70
	/**
71
	 * Plugin management object.
72
	 *
73
	 * @var Plugin
74
	 */
75
	private $plugin = null;
76
77
	/**
78
	 * Initialize the object.
79
	 * Make sure to call the "Configure" first.
80
	 *
81
	 * @param string $plugin_slug Slug of the plugin using the connection (optional, but encouraged).
82
	 *
83
	 * @see \Automattic\Jetpack\Config
84
	 */
85
	public function __construct( $plugin_slug = null ) {
86
		if ( $plugin_slug && is_string( $plugin_slug ) ) {
87
			$this->set_plugin_instance( new Plugin( $plugin_slug ) );
88
		}
89
	}
90
91
	/**
92
	 * Initializes required listeners. This is done separately from the constructors
93
	 * because some objects sometimes need to instantiate separate objects of this class.
94
	 *
95
	 * @todo Implement a proper nonce verification.
96
	 */
97
	public static function configure() {
98
		$manager = new self();
99
100
		add_filter(
101
			'jetpack_constant_default_value',
102
			__NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
103
			10,
104
			2
105
		);
106
107
		$manager->setup_xmlrpc_handlers(
108
			$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
109
			$manager->is_active(),
110
			$manager->verify_xml_rpc_signature()
111
		);
112
113
		$manager->error_handler = Error_Handler::get_instance();
114
115
		if ( $manager->is_active() ) {
116
			add_filter( 'xmlrpc_methods', array( $manager, 'public_xmlrpc_methods' ) );
117
		}
118
119
		add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ) );
120
121
		add_action( 'jetpack_clean_nonces', array( $manager, 'clean_nonces' ) );
122
		if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) {
123
			wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
124
		}
125
126
		add_action( 'plugins_loaded', __NAMESPACE__ . '\Plugin_Storage::configure', 100 );
127
128
		add_filter( 'map_meta_cap', array( $manager, 'jetpack_connection_custom_caps' ), 1, 4 );
129
130
		Heartbeat::init();
131
		add_filter( 'jetpack_heartbeat_stats_array', array( $manager, 'add_stats_to_heartbeat' ) );
132
133
	}
134
135
	/**
136
	 * Sets up the XMLRPC request handlers.
137
	 *
138
	 * @param array                  $request_params incoming request parameters.
139
	 * @param Boolean                $is_active whether the connection is currently active.
140
	 * @param Boolean                $is_signed whether the signature check has been successful.
141
	 * @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
142
	 */
143
	public function setup_xmlrpc_handlers(
144
		$request_params,
145
		$is_active,
146
		$is_signed,
147
		\Jetpack_XMLRPC_Server $xmlrpc_server = null
148
	) {
149
		add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 );
150
151
		if (
152
			! isset( $request_params['for'] )
153
			|| 'jetpack' !== $request_params['for']
154
		) {
155
			return false;
156
		}
157
158
		// Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
159
		if (
160
			isset( $request_params['jetpack'] )
161
			&& 'comms' === $request_params['jetpack']
162
		) {
163
			if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
164
				// Use the real constant here for WordPress' sake.
165
				define( 'XMLRPC_REQUEST', true );
166
			}
167
168
			add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) );
169
170
			add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 );
171
		}
172
173
		if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
174
			return false;
175
		}
176
		// Display errors can cause the XML to be not well formed.
177
		@ini_set( 'display_errors', false ); // phpcs:ignore
178
179
		if ( $xmlrpc_server ) {
180
			$this->xmlrpc_server = $xmlrpc_server;
181
		} else {
182
			$this->xmlrpc_server = new \Jetpack_XMLRPC_Server();
183
		}
184
185
		$this->require_jetpack_authentication();
186
187
		if ( $is_active ) {
188
			// Hack to preserve $HTTP_RAW_POST_DATA.
189
			add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
190
191
			if ( $is_signed ) {
192
				// The actual API methods.
193
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) );
194
			} else {
195
				// The jetpack.authorize method should be available for unauthenticated users on a site with an
196
				// active Jetpack connection, so that additional users can link their account.
197
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) );
198
			}
199
		} else {
200
			// The bootstrap API methods.
201
			add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) );
202
203
			if ( $is_signed ) {
204
				// The jetpack Provision method is available for blog-token-signed requests.
205
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) );
206
			} else {
207
				new XMLRPC_Connector( $this );
208
			}
209
		}
210
211
		// Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on.
212
		add_filter( 'pre_option_enable_xmlrpc', '__return_true' );
213
		return true;
214
	}
215
216
	/**
217
	 * Initializes the REST API connector on the init hook.
218
	 */
219
	public function initialize_rest_api_registration_connector() {
220
		new REST_Connector( $this );
221
	}
222
223
	/**
224
	 * Since a lot of hosts use a hammer approach to "protecting" WordPress sites,
225
	 * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive
226
	 * security/firewall policies, we provide our own alternate XML RPC API endpoint
227
	 * which is accessible via a different URI. Most of the below is copied directly
228
	 * from /xmlrpc.php so that we're replicating it as closely as possible.
229
	 *
230
	 * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
231
	 */
232
	public function alternate_xmlrpc() {
233
		// Some browser-embedded clients send cookies. We don't want them.
234
		$_COOKIE = array();
235
236
		include_once ABSPATH . 'wp-admin/includes/admin.php';
237
		include_once ABSPATH . WPINC . '/class-IXR.php';
238
		include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php';
239
240
		/**
241
		 * Filters the class used for handling XML-RPC requests.
242
		 *
243
		 * @since 3.1.0
244
		 *
245
		 * @param string $class The name of the XML-RPC server class.
246
		 */
247
		$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' );
248
		$wp_xmlrpc_server       = new $wp_xmlrpc_server_class();
249
250
		// Fire off the request.
251
		nocache_headers();
252
		$wp_xmlrpc_server->serve_request();
253
254
		exit;
255
	}
256
257
	/**
258
	 * Removes all XML-RPC methods that are not `jetpack.*`.
259
	 * Only used in our alternate XML-RPC endpoint, where we want to
260
	 * ensure that Core and other plugins' methods are not exposed.
261
	 *
262
	 * @param array $methods a list of registered WordPress XMLRPC methods.
263
	 * @return array filtered $methods
264
	 */
265
	public function remove_non_jetpack_xmlrpc_methods( $methods ) {
266
		$jetpack_methods = array();
267
268
		foreach ( $methods as $method => $callback ) {
269
			if ( 0 === strpos( $method, 'jetpack.' ) ) {
270
				$jetpack_methods[ $method ] = $callback;
271
			}
272
		}
273
274
		return $jetpack_methods;
275
	}
276
277
	/**
278
	 * Removes all other authentication methods not to allow other
279
	 * methods to validate unauthenticated requests.
280
	 */
281
	public function require_jetpack_authentication() {
282
		// Don't let anyone authenticate.
283
		$_COOKIE = array();
284
		remove_all_filters( 'authenticate' );
285
		remove_all_actions( 'wp_login_failed' );
286
287
		if ( $this->is_active() ) {
288
			// Allow Jetpack authentication.
289
			add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 );
290
		}
291
	}
292
293
	/**
294
	 * Authenticates XML-RPC and other requests from the Jetpack Server
295
	 *
296
	 * @param WP_User|Mixed $user user object if authenticated.
297
	 * @param String        $username username.
298
	 * @param String        $password password string.
299
	 * @return WP_User|Mixed authenticated user or error.
300
	 */
301
	public function authenticate_jetpack( $user, $username, $password ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
302
		if ( is_a( $user, '\\WP_User' ) ) {
303
			return $user;
304
		}
305
306
		$token_details = $this->verify_xml_rpc_signature();
307
308
		if ( ! $token_details ) {
309
			return $user;
310
		}
311
312
		if ( 'user' !== $token_details['type'] ) {
313
			return $user;
314
		}
315
316
		if ( ! $token_details['user_id'] ) {
317
			return $user;
318
		}
319
320
		nocache_headers();
321
322
		return new \WP_User( $token_details['user_id'] );
323
	}
324
325
	/**
326
	 * Verifies the signature of the current request.
327
	 *
328
	 * @return false|array
329
	 */
330
	public function verify_xml_rpc_signature() {
331
		if ( is_null( $this->xmlrpc_verification ) ) {
332
			$this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature();
333
334
			if ( is_wp_error( $this->xmlrpc_verification ) ) {
335
				/**
336
				 * Action for logging XMLRPC signature verification errors. This data is sensitive.
337
				 *
338
				 * @since 7.5.0
339
				 *
340
				 * @param WP_Error $signature_verification_error The verification error
341
				 */
342
				do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification );
343
344
				Error_Handler::get_instance()->report_error( $this->xmlrpc_verification );
345
346
			}
347
		}
348
349
		return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification;
350
	}
351
352
	/**
353
	 * Verifies the signature of the current request.
354
	 *
355
	 * This function has side effects and should not be used. Instead,
356
	 * use the memoized version `->verify_xml_rpc_signature()`.
357
	 *
358
	 * @internal
359
	 * @todo Refactor to use proper nonce verification.
360
	 */
361
	private function internal_verify_xml_rpc_signature() {
362
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
363
		// It's not for us.
364
		if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) {
365
			return false;
366
		}
367
368
		$signature_details = array(
369
			'token'     => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '',
370
			'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '',
371
			'nonce'     => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '',
372
			'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '',
373
			'method'    => wp_unslash( $_SERVER['REQUEST_METHOD'] ),
374
			'url'       => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later.
375
			'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '',
376
		);
377
378
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
379
		@list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) );
380
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
381
382
		$jetpack_api_version = Constants::get_constant( 'JETPACK__API_VERSION' );
383
384
		if (
385
			empty( $token_key )
386
		||
387
			empty( $version ) || (string) $jetpack_api_version !== $version ) {
388
			return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) );
389
		}
390
391
		if ( '0' === $user_id ) {
392
			$token_type = 'blog';
393
			$user_id    = 0;
394
		} else {
395
			$token_type = 'user';
396
			if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) {
397
				return new \WP_Error(
398
					'malformed_user_id',
399
					'Malformed user_id in request',
400
					compact( 'signature_details' )
401
				);
402
			}
403
			$user_id = (int) $user_id;
404
405
			$user = new \WP_User( $user_id );
406
			if ( ! $user || ! $user->exists() ) {
407
				return new \WP_Error(
408
					'unknown_user',
409
					sprintf( 'User %d does not exist', $user_id ),
410
					compact( 'signature_details' )
411
				);
412
			}
413
		}
414
415
		$token = $this->get_access_token( $user_id, $token_key, false );
416
		if ( is_wp_error( $token ) ) {
417
			$token->add_data( compact( 'signature_details' ) );
418
			return $token;
419
		} elseif ( ! $token ) {
420
			return new \WP_Error(
421
				'unknown_token',
422
				sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ),
423
				compact( 'signature_details' )
424
			);
425
		}
426
427
		$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
428
		// phpcs:disable WordPress.Security.NonceVerification.Missing
429
		if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
430
			$post_data   = $_POST;
431
			$file_hashes = array();
432
			foreach ( $post_data as $post_data_key => $post_data_value ) {
433
				if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) {
434
					continue;
435
				}
436
				$post_data_key                 = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
437
				$file_hashes[ $post_data_key ] = $post_data_value;
438
			}
439
440
			foreach ( $file_hashes as $post_data_key => $post_data_value ) {
441
				unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] );
442
				$post_data[ $post_data_key ] = $post_data_value;
443
			}
444
445
			ksort( $post_data );
446
447
			$body = http_build_query( stripslashes_deep( $post_data ) );
448
		} elseif ( is_null( $this->raw_post_data ) ) {
449
			$body = file_get_contents( 'php://input' );
450
		} else {
451
			$body = null;
452
		}
453
		// phpcs:enable
454
455
		$signature = $jetpack_signature->sign_current_request(
456
			array( 'body' => is_null( $body ) ? $this->raw_post_data : $body )
457
		);
458
459
		$signature_details['url'] = $jetpack_signature->current_request_url;
460
461
		if ( ! $signature ) {
462
			return new \WP_Error(
463
				'could_not_sign',
464
				'Unknown signature error',
465
				compact( 'signature_details' )
466
			);
467
		} elseif ( is_wp_error( $signature ) ) {
468
			return $signature;
469
		}
470
471
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
472
		$timestamp = (int) $_GET['timestamp'];
473
		$nonce     = stripslashes( (string) $_GET['nonce'] );
474
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
475
476
		// Use up the nonce regardless of whether the signature matches.
477
		if ( ! $this->add_nonce( $timestamp, $nonce ) ) {
478
			return new \WP_Error(
479
				'invalid_nonce',
480
				'Could not add nonce',
481
				compact( 'signature_details' )
482
			);
483
		}
484
485
		// Be careful about what you do with this debugging data.
486
		// If a malicious requester has access to the expected signature,
487
		// bad things might be possible.
488
		$signature_details['expected'] = $signature;
489
490
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
491
		if ( ! hash_equals( $signature, $_GET['signature'] ) ) {
492
			return new \WP_Error(
493
				'signature_mismatch',
494
				'Signature mismatch',
495
				compact( 'signature_details' )
496
			);
497
		}
498
499
		/**
500
		 * Action for additional token checking.
501
		 *
502
		 * @since 7.7.0
503
		 *
504
		 * @param array $post_data request data.
505
		 * @param array $token_data token data.
506
		 */
507
		return apply_filters(
508
			'jetpack_signature_check_token',
509
			array(
510
				'type'      => $token_type,
511
				'token_key' => $token_key,
512
				'user_id'   => $token->external_user_id,
513
			),
514
			$token,
515
			$this->raw_post_data
516
		);
517
	}
518
519
	/**
520
	 * Returns true if the current site is connected to WordPress.com and has the minimum requirements to enable Jetpack UI.
521
	 *
522
	 * @return Boolean is the site connected?
523
	 */
524
	public function is_active() {
525
		if ( ( new Status() )->is_no_user_testing_mode() ) {
526
			return $this->is_connected();
527
		}
528
		return (bool) $this->get_access_token( self::CONNECTION_OWNER );
529
	}
530
531
	/**
532
	 * Returns true if the site has both a token and a blog id, which indicates a site has been registered.
533
	 *
534
	 * @access public
535
	 * @deprecated 9.2.0 Use is_connected instead
536
	 * @see Manager::is_connected
537
	 *
538
	 * @return bool
539
	 */
540
	public function is_registered() {
541
		_deprecated_function( __METHOD__, 'jetpack-9.2' );
542
		return $this->is_connected();
543
	}
544
545
	/**
546
	 * Returns true if the site has both a token and a blog id, which indicates a site has been connected.
547
	 *
548
	 * @access public
549
	 * @since 9.2.0
550
	 *
551
	 * @return bool
552
	 */
553
	public function is_connected() {
554
		$has_blog_id    = (bool) \Jetpack_Options::get_option( 'id' );
555
		$has_blog_token = (bool) $this->get_access_token( false );
556
		return $has_blog_id && $has_blog_token;
557
	}
558
559
	/**
560
	 * Returns true if the site has at least one connected administrator.
561
	 *
562
	 * @access public
563
	 * @since 9.2.0
564
	 *
565
	 * @return bool
566
	 */
567
	public function has_connected_admin() {
568
		return (bool) count( $this->get_connected_users( 'manage_options' ) );
569
	}
570
571
	/**
572
	 * Returns true if the site has any connected user.
573
	 *
574
	 * @access public
575
	 * @since 9.2.0
576
	 *
577
	 * @return bool
578
	 */
579
	public function has_connected_user() {
580
		return (bool) count( $this->get_connected_users() );
581
	}
582
583
	/**
584
	 * Returns true if the site has a connected Blog owner (master_user).
585
	 *
586
	 * @access public
587
	 * @since 9.2.0
588
	 *
589
	 * @return bool
590
	 */
591
	public function has_connected_owner() {
592
		return (bool) $this->get_connection_owner_id();
593
	}
594
595
	/**
596
	 * Checks to see if the connection owner of the site is missing.
597
	 *
598
	 * @return bool
599
	 */
600
	public function is_missing_connection_owner() {
601
		$connection_owner = $this->get_connection_owner_id();
602
		if ( ! get_user_by( 'id', $connection_owner ) ) {
603
			return true;
604
		}
605
606
		return false;
607
	}
608
609
	/**
610
	 * Returns true if the user with the specified identifier is connected to
611
	 * WordPress.com.
612
	 *
613
	 * @param int $user_id the user identifier. Default is the current user.
0 ignored issues
show
Should the type for parameter $user_id not be false|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
614
	 * @return bool Boolean is the user connected?
615
	 */
616
	public function is_user_connected( $user_id = false ) {
617
		$user_id = false === $user_id ? get_current_user_id() : absint( $user_id );
618
		if ( ! $user_id ) {
619
			return false;
620
		}
621
622
		return (bool) $this->get_access_token( $user_id );
623
	}
624
625
	/**
626
	 * Returns the local user ID of the connection owner.
627
	 *
628
	 * @return bool|int Returns the ID of the connection owner or False if no connection owner found.
629
	 */
630
	public function get_connection_owner_id() {
631
		$owner = $this->get_connection_owner();
632
		return $owner instanceof \WP_User ? $owner->ID : false;
0 ignored issues
show
The class WP_User does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
633
	}
634
635
	/**
636
	 * Returns an array of user_id's that have user tokens for communicating with wpcom.
637
	 * Able to select by specific capability.
638
	 *
639
	 * @param string $capability The capability of the user.
640
	 * @return array Array of WP_User objects if found.
641
	 */
642
	public function get_connected_users( $capability = 'any' ) {
643
		$connected_users = array();
644
		$user_tokens     = \Jetpack_Options::get_option( 'user_tokens' );
645
646
		if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) {
647
			return $connected_users;
648
		}
649
		$connected_user_ids = array_keys( $user_tokens );
650
651
		if ( ! empty( $connected_user_ids ) ) {
652
			foreach ( $connected_user_ids as $id ) {
653
				// Check for capability.
654
				if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
655
					continue;
656
				}
657
658
				$user_data = get_userdata( $id );
659
				if ( $user_data instanceof \WP_User ) {
0 ignored issues
show
The class WP_User does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
660
					$connected_users[] = $user_data;
661
				}
662
			}
663
		}
664
665
		return $connected_users;
666
	}
667
668
	/**
669
	 * Get the wpcom user data of the current|specified connected user.
670
	 *
671
	 * @todo Refactor to properly load the XMLRPC client independently.
672
	 *
673
	 * @param Integer $user_id the user identifier.
674
	 * @return Object the user object.
675
	 */
676 View Code Duplication
	public function get_connected_user_data( $user_id = null ) {
677
		if ( ! $user_id ) {
678
			$user_id = get_current_user_id();
679
		}
680
681
		$transient_key    = "jetpack_connected_user_data_$user_id";
682
		$cached_user_data = get_transient( $transient_key );
683
684
		if ( $cached_user_data ) {
685
			return $cached_user_data;
686
		}
687
688
		$xml = new \Jetpack_IXR_Client(
689
			array(
690
				'user_id' => $user_id,
691
			)
692
		);
693
		$xml->query( 'wpcom.getUser' );
694
		if ( ! $xml->isError() ) {
695
			$user_data = $xml->getResponse();
696
			set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS );
697
			return $user_data;
698
		}
699
700
		return false;
701
	}
702
703
	/**
704
	 * Returns a user object of the connection owner.
705
	 *
706
	 * @return WP_User|false False if no connection owner found.
707
	 */
708
	public function get_connection_owner() {
709
710
		$user_id = \Jetpack_Options::get_option( 'master_user' );
711
712
		if ( ! $user_id ) {
713
			return false;
714
		}
715
716
		// Make sure user is connected.
717
		$user_token = $this->get_access_token( $user_id );
718
719
		$connection_owner = false;
720
721
		if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) {
722
			$connection_owner = get_userdata( $user_token->external_user_id );
723
		}
724
725
		return $connection_owner;
726
	}
727
728
	/**
729
	 * Returns true if the provided user is the Jetpack connection owner.
730
	 * If user ID is not specified, the current user will be used.
731
	 *
732
	 * @param Integer|Boolean $user_id the user identifier. False for current user.
733
	 * @return Boolean True the user the connection owner, false otherwise.
734
	 */
735
	public function is_connection_owner( $user_id = false ) {
736
		if ( ! $user_id ) {
737
			$user_id = get_current_user_id();
738
		}
739
740
		return ( (int) $user_id ) === $this->get_connection_owner_id();
741
	}
742
743
	/**
744
	 * Connects the user with a specified ID to a WordPress.com user using the
745
	 * remote login flow.
746
	 *
747
	 * @access public
748
	 *
749
	 * @param Integer $user_id (optional) the user identifier, defaults to current user.
750
	 * @param String  $redirect_url the URL to redirect the user to for processing, defaults to
751
	 *                              admin_url().
752
	 * @return WP_Error only in case of a failed user lookup.
753
	 */
754
	public function connect_user( $user_id = null, $redirect_url = null ) {
755
		$user = null;
756
		if ( null === $user_id ) {
757
			$user = wp_get_current_user();
758
		} else {
759
			$user = get_user_by( 'ID', $user_id );
760
		}
761
762
		if ( empty( $user ) ) {
763
			return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' );
764
		}
765
766
		if ( null === $redirect_url ) {
767
			$redirect_url = admin_url();
768
		}
769
770
		// Using wp_redirect intentionally because we're redirecting outside.
771
		wp_redirect( $this->get_authorization_url( $user ) ); // phpcs:ignore WordPress.Security.SafeRedirect
772
		exit();
773
	}
774
775
	/**
776
	 * Unlinks the current user from the linked WordPress.com user.
777
	 *
778
	 * @access public
779
	 * @static
780
	 *
781
	 * @todo Refactor to properly load the XMLRPC client independently.
782
	 *
783
	 * @param Integer $user_id the user identifier.
784
	 * @param bool    $can_overwrite_primary_user Allow for the primary user to be disconnected.
785
	 * @return Boolean Whether the disconnection of the user was successful.
786
	 */
787
	public static function disconnect_user( $user_id = null, $can_overwrite_primary_user = false ) {
788
		$tokens = Jetpack_Options::get_option( 'user_tokens' );
789
		if ( ! $tokens ) {
790
			return false;
791
		}
792
793
		$user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id;
794
795
		if ( Jetpack_Options::get_option( 'master_user' ) === $user_id && ! $can_overwrite_primary_user ) {
796
			return false;
797
		}
798
799
		if ( ! isset( $tokens[ $user_id ] ) ) {
800
			return false;
801
		}
802
803
		$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) );
804
		$xml->query( 'jetpack.unlink_user', $user_id );
805
806
		unset( $tokens[ $user_id ] );
807
808
		Jetpack_Options::update_option( 'user_tokens', $tokens );
809
810
		// Delete cached connected user data.
811
		$transient_key = "jetpack_connected_user_data_$user_id";
812
		delete_transient( $transient_key );
813
814
		/**
815
		 * Fires after the current user has been unlinked from WordPress.com.
816
		 *
817
		 * @since 4.1.0
818
		 *
819
		 * @param int $user_id The current user's ID.
820
		 */
821
		do_action( 'jetpack_unlinked_user', $user_id );
822
823
		return true;
824
	}
825
826
	/**
827
	 * Returns the requested Jetpack API URL.
828
	 *
829
	 * @param String $relative_url the relative API path.
830
	 * @return String API URL.
831
	 */
832
	public function api_url( $relative_url ) {
833
		$api_base    = Constants::get_constant( 'JETPACK__API_BASE' );
834
		$api_version = '/' . Constants::get_constant( 'JETPACK__API_VERSION' ) . '/';
835
836
		/**
837
		 * Filters whether the connection manager should use the iframe authorization
838
		 * flow instead of the regular redirect-based flow.
839
		 *
840
		 * @since 8.3.0
841
		 *
842
		 * @param Boolean $is_iframe_flow_used should the iframe flow be used, defaults to false.
843
		 */
844
		$iframe_flow = apply_filters( 'jetpack_use_iframe_authorization_flow', false );
845
846
		// Do not modify anything that is not related to authorize requests.
847
		if ( 'authorize' === $relative_url && $iframe_flow ) {
848
			$relative_url = 'authorize_iframe';
849
		}
850
851
		/**
852
		 * Filters the API URL that Jetpack uses for server communication.
853
		 *
854
		 * @since 8.0.0
855
		 *
856
		 * @param String $url the generated URL.
857
		 * @param String $relative_url the relative URL that was passed as an argument.
858
		 * @param String $api_base the API base string that is being used.
859
		 * @param String $api_version the API version string that is being used.
860
		 */
861
		return apply_filters(
862
			'jetpack_api_url',
863
			rtrim( $api_base . $relative_url, '/\\' ) . $api_version,
864
			$relative_url,
865
			$api_base,
866
			$api_version
867
		);
868
	}
869
870
	/**
871
	 * Returns the Jetpack XMLRPC WordPress.com API endpoint URL.
872
	 *
873
	 * @return String XMLRPC API URL.
874
	 */
875
	public function xmlrpc_api_url() {
876
		$base = preg_replace(
877
			'#(https?://[^?/]+)(/?.*)?$#',
878
			'\\1',
879
			Constants::get_constant( 'JETPACK__API_BASE' )
880
		);
881
		return untrailingslashit( $base ) . '/xmlrpc.php';
882
	}
883
884
	/**
885
	 * Attempts Jetpack registration which sets up the site for connection. Should
886
	 * remain public because the call to action comes from the current site, not from
887
	 * WordPress.com.
888
	 *
889
	 * @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'.
890
	 * @return true|WP_Error The error object.
891
	 */
892
	public function register( $api_endpoint = 'register' ) {
893
		add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) );
894
		$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 );
895
896
		if ( false === $secrets ) {
897
			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' ) );
898
		}
899
900
		if (
901
			empty( $secrets['secret_1'] ) ||
902
			empty( $secrets['secret_2'] ) ||
903
			empty( $secrets['exp'] )
904
		) {
905
			return new \WP_Error( 'missing_secrets' );
906
		}
907
908
		// Better to try (and fail) to set a higher timeout than this system
909
		// supports than to have register fail for more users than it should.
910
		$timeout = $this->set_min_time_limit( 60 ) / 2;
911
912
		$gmt_offset = get_option( 'gmt_offset' );
913
		if ( ! $gmt_offset ) {
914
			$gmt_offset = 0;
915
		}
916
917
		$stats_options = get_option( 'stats_options' );
918
		$stats_id      = isset( $stats_options['blog_id'] )
919
			? $stats_options['blog_id']
920
			: null;
921
922
		/**
923
		 * Filters the request body for additional property addition.
924
		 *
925
		 * @since 7.7.0
926
		 *
927
		 * @param array $post_data request data.
928
		 * @param Array $token_data token data.
929
		 */
930
		$body = apply_filters(
931
			'jetpack_register_request_body',
932
			array(
933
				'siteurl'            => site_url(),
934
				'home'               => home_url(),
935
				'gmt_offset'         => $gmt_offset,
936
				'timezone_string'    => (string) get_option( 'timezone_string' ),
937
				'site_name'          => (string) get_option( 'blogname' ),
938
				'secret_1'           => $secrets['secret_1'],
939
				'secret_2'           => $secrets['secret_2'],
940
				'site_lang'          => get_locale(),
941
				'timeout'            => $timeout,
942
				'stats_id'           => $stats_id,
943
				'state'              => get_current_user_id(),
944
				'site_created'       => $this->get_assumed_site_creation_date(),
945
				'jetpack_version'    => Constants::get_constant( 'JETPACK__VERSION' ),
946
				'ABSPATH'            => Constants::get_constant( 'ABSPATH' ),
947
				'current_user_email' => wp_get_current_user()->user_email,
948
				'connect_plugin'     => $this->get_plugin() ? $this->get_plugin()->get_slug() : null,
949
			)
950
		);
951
952
		$args = array(
953
			'method'  => 'POST',
954
			'body'    => $body,
955
			'headers' => array(
956
				'Accept' => 'application/json',
957
			),
958
			'timeout' => $timeout,
959
		);
960
961
		$args['body'] = $this->apply_activation_source_to_args( $args['body'] );
962
963
		// TODO: fix URLs for bad hosts.
964
		$response = Client::_wp_remote_request(
965
			$this->api_url( $api_endpoint ),
966
			$args,
967
			true
968
		);
969
970
		// Make sure the response is valid and does not contain any Jetpack errors.
971
		$registration_details = $this->validate_remote_register_response( $response );
972
973
		if ( is_wp_error( $registration_details ) ) {
974
			return $registration_details;
975
		} elseif ( ! $registration_details ) {
976
			return new \WP_Error(
977
				'unknown_error',
978
				'Unknown error registering your Jetpack site.',
979
				wp_remote_retrieve_response_code( $response )
980
			);
981
		}
982
983
		if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) {
984
			return new \WP_Error(
985
				'jetpack_secret',
986
				'Unable to validate registration of your Jetpack site.',
987
				wp_remote_retrieve_response_code( $response )
988
			);
989
		}
990
991
		if ( isset( $registration_details->jetpack_public ) ) {
992
			$jetpack_public = (int) $registration_details->jetpack_public;
993
		} else {
994
			$jetpack_public = false;
995
		}
996
997
		\Jetpack_Options::update_options(
998
			array(
999
				'id'         => (int) $registration_details->jetpack_id,
1000
				'blog_token' => (string) $registration_details->jetpack_secret,
1001
				'public'     => $jetpack_public,
1002
			)
1003
		);
1004
1005
		/**
1006
		 * Fires when a site is registered on WordPress.com.
1007
		 *
1008
		 * @since 3.7.0
1009
		 *
1010
		 * @param int $json->jetpack_id Jetpack Blog ID.
1011
		 * @param string $json->jetpack_secret Jetpack Blog Token.
1012
		 * @param int|bool $jetpack_public Is the site public.
1013
		 */
1014
		do_action(
1015
			'jetpack_site_registered',
1016
			$registration_details->jetpack_id,
1017
			$registration_details->jetpack_secret,
1018
			$jetpack_public
1019
		);
1020
1021
		if ( isset( $registration_details->token ) ) {
1022
			/**
1023
			 * Fires when a user token is sent along with the registration data.
1024
			 *
1025
			 * @since 7.6.0
1026
			 *
1027
			 * @param object $token the administrator token for the newly registered site.
1028
			 */
1029
			do_action( 'jetpack_site_registered_user_token', $registration_details->token );
1030
		}
1031
1032
		return true;
1033
	}
1034
1035
	/**
1036
	 * Takes the response from the Jetpack register new site endpoint and
1037
	 * verifies it worked properly.
1038
	 *
1039
	 * @since 2.6
1040
	 *
1041
	 * @param Mixed $response the response object, or the error object.
1042
	 * @return string|WP_Error A JSON object on success or WP_Error on failures
1043
	 **/
1044
	protected function validate_remote_register_response( $response ) {
1045
		if ( is_wp_error( $response ) ) {
1046
			return new \WP_Error(
1047
				'register_http_request_failed',
1048
				$response->get_error_message()
1049
			);
1050
		}
1051
1052
		$code   = wp_remote_retrieve_response_code( $response );
1053
		$entity = wp_remote_retrieve_body( $response );
1054
1055
		if ( $entity ) {
1056
			$registration_response = json_decode( $entity );
1057
		} else {
1058
			$registration_response = false;
1059
		}
1060
1061
		$code_type = (int) ( $code / 100 );
1062
		if ( 5 === $code_type ) {
1063
			return new \WP_Error( 'wpcom_5??', $code );
1064
		} elseif ( 408 === $code ) {
1065
			return new \WP_Error( 'wpcom_408', $code );
1066
		} elseif ( ! empty( $registration_response->error ) ) {
1067
			if (
1068
				'xml_rpc-32700' === $registration_response->error
1069
				&& ! function_exists( 'xml_parser_create' )
1070
			) {
1071
				$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' );
1072
			} else {
1073
				$error_description = isset( $registration_response->error_description )
1074
					? (string) $registration_response->error_description
1075
					: '';
1076
			}
1077
1078
			return new \WP_Error(
1079
				(string) $registration_response->error,
1080
				$error_description,
1081
				$code
1082
			);
1083
		} elseif ( 200 !== $code ) {
1084
			return new \WP_Error( 'wpcom_bad_response', $code );
1085
		}
1086
1087
		// Jetpack ID error block.
1088
		if ( empty( $registration_response->jetpack_id ) ) {
1089
			return new \WP_Error(
1090
				'jetpack_id',
1091
				/* translators: %s is an error message string */
1092
				sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1093
				$entity
1094
			);
1095
		} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) {
1096
			return new \WP_Error(
1097
				'jetpack_id',
1098
				/* translators: %s is an error message string */
1099
				sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1100
				$entity
1101
			);
1102 View Code Duplication
		} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) {
1103
			return new \WP_Error(
1104
				'jetpack_id',
1105
				/* translators: %s is an error message string */
1106
				sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1107
				$entity
1108
			);
1109
		}
1110
1111
		return $registration_response;
1112
	}
1113
1114
	/**
1115
	 * Adds a used nonce to a list of known nonces.
1116
	 *
1117
	 * @param int    $timestamp the current request timestamp.
1118
	 * @param string $nonce the nonce value.
1119
	 * @return bool whether the nonce is unique or not.
1120
	 */
1121
	public function add_nonce( $timestamp, $nonce ) {
1122
		global $wpdb;
1123
		static $nonces_used_this_request = array();
1124
1125
		if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) {
1126
			return $nonces_used_this_request[ "$timestamp:$nonce" ];
1127
		}
1128
1129
		// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce.
1130
		$timestamp = (int) $timestamp;
1131
		$nonce     = esc_sql( $nonce );
1132
1133
		// Raw query so we can avoid races: add_option will also update.
1134
		$show_errors = $wpdb->show_errors( false );
1135
1136
		$old_nonce = $wpdb->get_row(
1137
			$wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" )
1138
		);
1139
1140
		if ( is_null( $old_nonce ) ) {
1141
			$return = $wpdb->query(
1142
				$wpdb->prepare(
1143
					"INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)",
1144
					"jetpack_nonce_{$timestamp}_{$nonce}",
1145
					time(),
1146
					'no'
1147
				)
1148
			);
1149
		} else {
1150
			$return = false;
1151
		}
1152
1153
		$wpdb->show_errors( $show_errors );
1154
1155
		$nonces_used_this_request[ "$timestamp:$nonce" ] = $return;
1156
1157
		return $return;
1158
	}
1159
1160
	/**
1161
	 * Cleans nonces that were saved when calling ::add_nonce.
1162
	 *
1163
	 * @todo Properly prepare the query before executing it.
1164
	 *
1165
	 * @param bool $all whether to clean even non-expired nonces.
1166
	 */
1167
	public function clean_nonces( $all = false ) {
1168
		global $wpdb;
1169
1170
		$sql      = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s";
1171
		$sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' );
1172
1173
		if ( true !== $all ) {
1174
			$sql       .= ' AND CAST( `option_value` AS UNSIGNED ) < %d';
1175
			$sql_args[] = time() - 3600;
1176
		}
1177
1178
		$sql .= ' ORDER BY `option_id` LIMIT 100';
1179
1180
		$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1181
1182
		for ( $i = 0; $i < 1000; $i++ ) {
1183
			if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1184
				break;
1185
			}
1186
		}
1187
	}
1188
1189
	/**
1190
	 * Sets the Connection custom capabilities.
1191
	 *
1192
	 * @param string[] $caps    Array of the user's capabilities.
1193
	 * @param string   $cap     Capability name.
1194
	 * @param int      $user_id The user ID.
1195
	 * @param array    $args    Adds the context to the cap. Typically the object ID.
1196
	 */
1197
	public function jetpack_connection_custom_caps( $caps, $cap, $user_id, $args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1198
		$is_offline_mode = ( new Status() )->is_offline_mode();
1199
		switch ( $cap ) {
1200
			case 'jetpack_connect':
1201
			case 'jetpack_reconnect':
1202
				if ( $is_offline_mode ) {
1203
					$caps = array( 'do_not_allow' );
1204
					break;
1205
				}
1206
				// Pass through. If it's not offline mode, these should match disconnect.
1207
				// Let users disconnect if it's offline mode, just in case things glitch.
1208
			case 'jetpack_disconnect':
1209
				/**
1210
				 * Filters the jetpack_disconnect capability.
1211
				 *
1212
				 * @since 8.7.0
1213
				 *
1214
				 * @param array An array containing the capability name.
1215
				 */
1216
				$caps = apply_filters( 'jetpack_disconnect_cap', array( 'manage_options' ) );
1217
				break;
1218
			case 'jetpack_connect_user':
1219
				if ( $is_offline_mode ) {
1220
					$caps = array( 'do_not_allow' );
1221
					break;
1222
				}
1223
				$caps = array( 'read' );
1224
				break;
1225
		}
1226
		return $caps;
1227
	}
1228
1229
	/**
1230
	 * Builds the timeout limit for queries talking with the wpcom servers.
1231
	 *
1232
	 * Based on local php max_execution_time in php.ini
1233
	 *
1234
	 * @since 5.4
1235
	 * @return int
1236
	 **/
1237
	public function get_max_execution_time() {
1238
		$timeout = (int) ini_get( 'max_execution_time' );
1239
1240
		// Ensure exec time set in php.ini.
1241
		if ( ! $timeout ) {
1242
			$timeout = 30;
1243
		}
1244
		return $timeout;
1245
	}
1246
1247
	/**
1248
	 * Sets a minimum request timeout, and returns the current timeout
1249
	 *
1250
	 * @since 5.4
1251
	 * @param Integer $min_timeout the minimum timeout value.
1252
	 **/
1253 View Code Duplication
	public function set_min_time_limit( $min_timeout ) {
1254
		$timeout = $this->get_max_execution_time();
1255
		if ( $timeout < $min_timeout ) {
1256
			$timeout = $min_timeout;
1257
			set_time_limit( $timeout );
1258
		}
1259
		return $timeout;
1260
	}
1261
1262
	/**
1263
	 * Get our assumed site creation date.
1264
	 * Calculated based on the earlier date of either:
1265
	 * - Earliest admin user registration date.
1266
	 * - Earliest date of post of any post type.
1267
	 *
1268
	 * @since 7.2.0
1269
	 *
1270
	 * @return string Assumed site creation date and time.
1271
	 */
1272
	public function get_assumed_site_creation_date() {
1273
		$cached_date = get_transient( 'jetpack_assumed_site_creation_date' );
1274
		if ( ! empty( $cached_date ) ) {
1275
			return $cached_date;
1276
		}
1277
1278
		$earliest_registered_users  = get_users(
1279
			array(
1280
				'role'    => 'administrator',
1281
				'orderby' => 'user_registered',
1282
				'order'   => 'ASC',
1283
				'fields'  => array( 'user_registered' ),
1284
				'number'  => 1,
1285
			)
1286
		);
1287
		$earliest_registration_date = $earliest_registered_users[0]->user_registered;
1288
1289
		$earliest_posts = get_posts(
1290
			array(
1291
				'posts_per_page' => 1,
1292
				'post_type'      => 'any',
1293
				'post_status'    => 'any',
1294
				'orderby'        => 'date',
1295
				'order'          => 'ASC',
1296
			)
1297
		);
1298
1299
		// If there are no posts at all, we'll count only on user registration date.
1300
		if ( $earliest_posts ) {
1301
			$earliest_post_date = $earliest_posts[0]->post_date;
1302
		} else {
1303
			$earliest_post_date = PHP_INT_MAX;
1304
		}
1305
1306
		$assumed_date = min( $earliest_registration_date, $earliest_post_date );
1307
		set_transient( 'jetpack_assumed_site_creation_date', $assumed_date );
1308
1309
		return $assumed_date;
1310
	}
1311
1312
	/**
1313
	 * Adds the activation source string as a parameter to passed arguments.
1314
	 *
1315
	 * @todo Refactor to use rawurlencode() instead of urlencode().
1316
	 *
1317
	 * @param array $args arguments that need to have the source added.
1318
	 * @return array $amended arguments.
1319
	 */
1320 View Code Duplication
	public static function apply_activation_source_to_args( $args ) {
1321
		list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' );
1322
1323
		if ( $activation_source_name ) {
1324
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1325
			$args['_as'] = urlencode( $activation_source_name );
1326
		}
1327
1328
		if ( $activation_source_keyword ) {
1329
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1330
			$args['_ak'] = urlencode( $activation_source_keyword );
1331
		}
1332
1333
		return $args;
1334
	}
1335
1336
	/**
1337
	 * Returns the callable that would be used to generate secrets.
1338
	 *
1339
	 * @return Callable a function that returns a secure string to be used as a secret.
1340
	 */
1341
	protected function get_secret_callable() {
1342
		if ( ! isset( $this->secret_callable ) ) {
1343
			/**
1344
			 * Allows modification of the callable that is used to generate connection secrets.
1345
			 *
1346
			 * @param Callable a function or method that returns a secret string.
1347
			 */
1348
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', array( $this, 'secret_callable_method' ) );
1349
		}
1350
1351
		return $this->secret_callable;
1352
	}
1353
1354
	/**
1355
	 * Runs the wp_generate_password function with the required parameters. This is the
1356
	 * default implementation of the secret callable, can be overridden using the
1357
	 * jetpack_connection_secret_generator filter.
1358
	 *
1359
	 * @return String $secret value.
1360
	 */
1361
	private function secret_callable_method() {
1362
		return wp_generate_password( 32, false );
1363
	}
1364
1365
	/**
1366
	 * Generates two secret tokens and the end of life timestamp for them.
1367
	 *
1368
	 * @param String  $action  The action name.
1369
	 * @param Integer $user_id The user identifier.
1370
	 * @param Integer $exp     Expiration time in seconds.
1371
	 */
1372
	public function generate_secrets( $action, $user_id = false, $exp = 600 ) {
1373
		if ( false === $user_id ) {
1374
			$user_id = get_current_user_id();
1375
		}
1376
1377
		$callable = $this->get_secret_callable();
1378
1379
		$secrets = \Jetpack_Options::get_raw_option(
1380
			self::SECRETS_OPTION_NAME,
1381
			array()
1382
		);
1383
1384
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1385
1386
		if (
1387
			isset( $secrets[ $secret_name ] ) &&
1388
			$secrets[ $secret_name ]['exp'] > time()
1389
		) {
1390
			return $secrets[ $secret_name ];
1391
		}
1392
1393
		$secret_value = array(
1394
			'secret_1' => call_user_func( $callable ),
1395
			'secret_2' => call_user_func( $callable ),
1396
			'exp'      => time() + $exp,
1397
		);
1398
1399
		$secrets[ $secret_name ] = $secret_value;
1400
1401
		$res = Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1402
		return $res ? $secrets[ $secret_name ] : false;
1403
	}
1404
1405
	/**
1406
	 * Returns two secret tokens and the end of life timestamp for them.
1407
	 *
1408
	 * @param String  $action  The action name.
1409
	 * @param Integer $user_id The user identifier.
1410
	 * @return string|array an array of secrets or an error string.
1411
	 */
1412
	public function get_secrets( $action, $user_id ) {
1413
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1414
		$secrets     = \Jetpack_Options::get_raw_option(
1415
			self::SECRETS_OPTION_NAME,
1416
			array()
1417
		);
1418
1419
		if ( ! isset( $secrets[ $secret_name ] ) ) {
1420
			return self::SECRETS_MISSING;
1421
		}
1422
1423
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
1424
			$this->delete_secrets( $action, $user_id );
1425
			return self::SECRETS_EXPIRED;
1426
		}
1427
1428
		return $secrets[ $secret_name ];
1429
	}
1430
1431
	/**
1432
	 * Deletes secret tokens in case they, for example, have expired.
1433
	 *
1434
	 * @param String  $action  The action name.
1435
	 * @param Integer $user_id The user identifier.
1436
	 */
1437
	public function delete_secrets( $action, $user_id ) {
1438
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1439
		$secrets     = \Jetpack_Options::get_raw_option(
1440
			self::SECRETS_OPTION_NAME,
1441
			array()
1442
		);
1443
		if ( isset( $secrets[ $secret_name ] ) ) {
1444
			unset( $secrets[ $secret_name ] );
1445
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1446
		}
1447
	}
1448
1449
	/**
1450
	 * Deletes all connection tokens and transients from the local Jetpack site.
1451
	 * If the plugin object has been provided in the constructor, the function first checks
1452
	 * whether it's the only active connection.
1453
	 * If there are any other connections, the function will do nothing and return `false`
1454
	 * (unless `$ignore_connected_plugins` is set to `true`).
1455
	 *
1456
	 * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
1457
	 *
1458
	 * @return bool True if disconnected successfully, false otherwise.
1459
	 */
1460
	public function delete_all_connection_tokens( $ignore_connected_plugins = false ) {
1461 View Code Duplication
		if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) {
1462
			return false;
1463
		}
1464
1465
		/**
1466
		 * Fires upon the disconnect attempt.
1467
		 * Return `false` to prevent the disconnect.
1468
		 *
1469
		 * @since 8.7.0
1470
		 */
1471
		if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true, $this ) ) {
1472
			return false;
1473
		}
1474
1475
		\Jetpack_Options::delete_option(
1476
			array(
1477
				'blog_token',
1478
				'user_token',
1479
				'user_tokens',
1480
				'master_user',
1481
				'time_diff',
1482
				'fallback_no_verify_ssl_certs',
1483
			)
1484
		);
1485
1486
		\Jetpack_Options::delete_raw_option( 'jetpack_secrets' );
1487
1488
		// Delete cached connected user data.
1489
		$transient_key = 'jetpack_connected_user_data_' . get_current_user_id();
1490
		delete_transient( $transient_key );
1491
1492
		// Delete all XML-RPC errors.
1493
		Error_Handler::get_instance()->delete_all_errors();
1494
1495
		return true;
1496
	}
1497
1498
	/**
1499
	 * Tells WordPress.com to disconnect the site and clear all tokens from cached site.
1500
	 * If the plugin object has been provided in the constructor, the function first check
1501
	 * whether it's the only active connection.
1502
	 * If there are any other connections, the function will do nothing and return `false`
1503
	 * (unless `$ignore_connected_plugins` is set to `true`).
1504
	 *
1505
	 * @param bool $ignore_connected_plugins Delete the tokens even if there are other connected plugins.
1506
	 *
1507
	 * @return bool True if disconnected successfully, false otherwise.
1508
	 */
1509
	public function disconnect_site_wpcom( $ignore_connected_plugins = false ) {
1510 View Code Duplication
		if ( ! $ignore_connected_plugins && null !== $this->plugin && ! $this->plugin->is_only() ) {
1511
			return false;
1512
		}
1513
1514
		/**
1515
		 * Fires upon the disconnect attempt.
1516
		 * Return `false` to prevent the disconnect.
1517
		 *
1518
		 * @since 8.7.0
1519
		 */
1520
		if ( ! apply_filters( 'jetpack_connection_disconnect_site_wpcom', true, $this ) ) {
1521
			return false;
1522
		}
1523
1524
		$xml = new \Jetpack_IXR_Client();
1525
		$xml->query( 'jetpack.deregister', get_current_user_id() );
1526
1527
		return true;
1528
	}
1529
1530
	/**
1531
	 * Disconnect the plugin and remove the tokens.
1532
	 * This function will automatically perform "soft" or "hard" disconnect depending on whether other plugins are using the connection.
1533
	 * This is a proxy method to simplify the Connection package API.
1534
	 *
1535
	 * @see Manager::disable_plugin()
1536
	 * @see Manager::disconnect_site_wpcom()
1537
	 * @see Manager::delete_all_connection_tokens()
1538
	 *
1539
	 * @return bool
1540
	 */
1541
	public function remove_connection() {
1542
		$this->disable_plugin();
1543
		$this->disconnect_site_wpcom();
1544
		$this->delete_all_connection_tokens();
1545
1546
		return true;
1547
	}
1548
1549
	/**
1550
	 * Completely clearing up the connection, and initiating reconnect.
1551
	 *
1552
	 * @return true|WP_Error True if reconnected successfully, a `WP_Error` object otherwise.
1553
	 */
1554
	public function reconnect() {
1555
		( new Tracking() )->record_user_event( 'restore_connection_reconnect' );
1556
1557
		$this->disconnect_site_wpcom( true );
1558
		$this->delete_all_connection_tokens( true );
1559
1560
		return $this->register();
1561
	}
1562
1563
	/**
1564
	 * Validate the tokens, and refresh the invalid ones.
1565
	 *
1566
	 * @return string|true|WP_Error True if connection restored or string indicating what's to be done next. A `WP_Error` object otherwise.
1567
	 */
1568
	public function restore() {
1569
		$invalid_tokens = array();
1570
		$can_restore    = $this->can_restore( $invalid_tokens );
1571
1572
		// Tokens are valid. We can't fix the problem we don't see, so the full reconnection is needed.
1573
		if ( ! $can_restore ) {
1574
			$result = $this->reconnect();
1575
			return true === $result ? 'authorize' : $result;
1576
		}
1577
1578
		if ( in_array( 'blog', $invalid_tokens, true ) ) {
1579
			return self::refresh_blog_token();
1580
		}
1581
1582
		if ( in_array( 'user', $invalid_tokens, true ) ) {
1583
			return true === self::refresh_user_token() ? 'authorize' : false;
1584
		}
1585
1586
		return false;
1587
	}
1588
1589
	/**
1590
	 * Determine whether we can restore the connection, or the full reconnect is needed.
1591
	 *
1592
	 * @param array $invalid_tokens The array the invalid tokens are stored in, provided by reference.
1593
	 *
1594
	 * @return bool `True` if the connection can be restored, `false` otherwise.
1595
	 */
1596
	public function can_restore( &$invalid_tokens ) {
1597
		$invalid_tokens = array();
1598
1599
		$validated_tokens = $this->validate_tokens();
1600
1601
		if ( ! is_array( $validated_tokens ) || count( array_diff_key( array_flip( array( 'blog_token', 'user_token' ) ), $validated_tokens ) ) ) {
1602
			return false;
1603
		}
1604
1605
		if ( empty( $validated_tokens['blog_token']['is_healthy'] ) ) {
1606
			$invalid_tokens[] = 'blog';
1607
		}
1608
1609
		if ( empty( $validated_tokens['user_token']['is_healthy'] ) ) {
1610
			$invalid_tokens[] = 'user';
1611
		}
1612
1613
		// If both tokens are invalid, we can't restore the connection.
1614
		return 1 === count( $invalid_tokens );
1615
	}
1616
1617
	/**
1618
	 * Perform the API request to validate the blog and user tokens.
1619
	 *
1620
	 * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
1621
	 *
1622
	 * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
1623
	 */
1624
	public function validate_tokens( $user_id = null ) {
1625
		$blog_id = Jetpack_Options::get_option( 'id' );
1626
		if ( ! $blog_id ) {
1627
			return new WP_Error( 'site_not_registered', 'Site not registered.' );
1628
		}
1629
		$url = sprintf(
1630
			'%s/%s/v%s/%s',
1631
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
1632
			'wpcom',
1633
			'2',
1634
			'sites/' . $blog_id . '/jetpack-token-health'
1635
		);
1636
1637
		$user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() );
1638
		$blog_token = $this->get_access_token();
1639
		$method     = 'POST';
1640
		$body       = array(
1641
			'user_token' => $this->get_signed_token( $user_token ),
1642
			'blog_token' => $this->get_signed_token( $blog_token ),
1643
		);
1644
		$response   = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
1645
1646
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
1647
			return false;
1648
		}
1649
1650
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
1651
1652
		return $body ? $body : false;
1653
	}
1654
1655
	/**
1656
	 * Responds to a WordPress.com call to register the current site.
1657
	 * Should be changed to protected.
1658
	 *
1659
	 * @param array $registration_data Array of [ secret_1, user_id ].
1660
	 */
1661
	public function handle_registration( array $registration_data ) {
1662
		list( $registration_secret_1, $registration_user_id ) = $registration_data;
1663
		if ( empty( $registration_user_id ) ) {
1664
			return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 );
1665
		}
1666
1667
		return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id );
1668
	}
1669
1670
	/**
1671
	 * Verify a Previously Generated Secret.
1672
	 *
1673
	 * @param string $action   The type of secret to verify.
1674
	 * @param string $secret_1 The secret string to compare to what is stored.
1675
	 * @param int    $user_id  The user ID of the owner of the secret.
1676
	 * @return \WP_Error|string WP_Error on failure, secret_2 on success.
1677
	 */
1678
	public function verify_secrets( $action, $secret_1, $user_id ) {
1679
		$allowed_actions = array( 'register', 'authorize', 'publicize' );
1680
		if ( ! in_array( $action, $allowed_actions, true ) ) {
1681
			return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
1682
		}
1683
1684
		$user = get_user_by( 'id', $user_id );
1685
1686
		/**
1687
		 * We've begun verifying the previously generated secret.
1688
		 *
1689
		 * @since 7.5.0
1690
		 *
1691
		 * @param string   $action The type of secret to verify.
1692
		 * @param \WP_User $user The user object.
1693
		 */
1694
		do_action( 'jetpack_verify_secrets_begin', $action, $user );
1695
1696
		$return_error = function ( \WP_Error $error ) use ( $action, $user ) {
1697
			/**
1698
			 * Verifying of the previously generated secret has failed.
1699
			 *
1700
			 * @since 7.5.0
1701
			 *
1702
			 * @param string    $action  The type of secret to verify.
1703
			 * @param \WP_User  $user The user object.
1704
			 * @param \WP_Error $error The error object.
1705
			 */
1706
			do_action( 'jetpack_verify_secrets_fail', $action, $user, $error );
1707
1708
			return $error;
1709
		};
1710
1711
		$stored_secrets = $this->get_secrets( $action, $user_id );
1712
		$this->delete_secrets( $action, $user_id );
1713
1714
		$error = null;
1715
		if ( empty( $secret_1 ) ) {
1716
			$error = $return_error(
1717
				new \WP_Error(
1718
					'verify_secret_1_missing',
1719
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1720
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ),
1721
					400
1722
				)
1723
			);
1724
		} elseif ( ! is_string( $secret_1 ) ) {
1725
			$error = $return_error(
1726
				new \WP_Error(
1727
					'verify_secret_1_malformed',
1728
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1729
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ),
1730
					400
1731
				)
1732
			);
1733
		} elseif ( empty( $user_id ) ) {
1734
			// $user_id is passed around during registration as "state".
1735
			$error = $return_error(
1736
				new \WP_Error(
1737
					'state_missing',
1738
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1739
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ),
1740
					400
1741
				)
1742
			);
1743
		} elseif ( ! ctype_digit( (string) $user_id ) ) {
1744
			$error = $return_error(
1745
				new \WP_Error(
1746
					'state_malformed',
1747
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1748
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ),
1749
					400
1750
				)
1751
			);
1752
		} elseif ( self::SECRETS_MISSING === $stored_secrets ) {
1753
			$error = $return_error(
1754
				new \WP_Error(
1755
					'verify_secrets_missing',
1756
					__( 'Verification secrets not found', 'jetpack' ),
1757
					400
1758
				)
1759
			);
1760
		} elseif ( self::SECRETS_EXPIRED === $stored_secrets ) {
1761
			$error = $return_error(
1762
				new \WP_Error(
1763
					'verify_secrets_expired',
1764
					__( 'Verification took too long', 'jetpack' ),
1765
					400
1766
				)
1767
			);
1768
		} elseif ( ! $stored_secrets ) {
1769
			$error = $return_error(
1770
				new \WP_Error(
1771
					'verify_secrets_empty',
1772
					__( 'Verification secrets are empty', 'jetpack' ),
1773
					400
1774
				)
1775
			);
1776
		} elseif ( is_wp_error( $stored_secrets ) ) {
1777
			$stored_secrets->add_data( 400 );
1778
			$error = $return_error( $stored_secrets );
1779
		} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
1780
			$error = $return_error(
1781
				new \WP_Error(
1782
					'verify_secrets_incomplete',
1783
					__( 'Verification secrets are incomplete', 'jetpack' ),
1784
					400
1785
				)
1786
			);
1787
		} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
1788
			$error = $return_error(
1789
				new \WP_Error(
1790
					'verify_secrets_mismatch',
1791
					__( 'Secret mismatch', 'jetpack' ),
1792
					400
1793
				)
1794
			);
1795
		}
1796
1797
		// Something went wrong during the checks, returning the error.
1798
		if ( ! empty( $error ) ) {
1799
			return $error;
1800
		}
1801
1802
		/**
1803
		 * We've succeeded at verifying the previously generated secret.
1804
		 *
1805
		 * @since 7.5.0
1806
		 *
1807
		 * @param string   $action The type of secret to verify.
1808
		 * @param \WP_User $user The user object.
1809
		 */
1810
		do_action( 'jetpack_verify_secrets_success', $action, $user );
1811
1812
		return $stored_secrets['secret_2'];
1813
	}
1814
1815
	/**
1816
	 * Responds to a WordPress.com call to authorize the current user.
1817
	 * Should be changed to protected.
1818
	 */
1819
	public function handle_authorization() {
1820
1821
	}
1822
1823
	/**
1824
	 * Obtains the auth token.
1825
	 *
1826
	 * @param array $data The request data.
1827
	 * @return object|\WP_Error Returns the auth token on success.
1828
	 *                          Returns a \WP_Error on failure.
1829
	 */
1830
	public function get_token( $data ) {
1831
		$roles = new Roles();
1832
		$role  = $roles->translate_current_user_to_role();
1833
1834
		if ( ! $role ) {
1835
			return new \WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack' ) );
1836
		}
1837
1838
		$client_secret = $this->get_access_token();
1839
		if ( ! $client_secret ) {
1840
			return new \WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack' ) );
1841
		}
1842
1843
		/**
1844
		 * Filter the URL of the first time the user gets redirected back to your site for connection
1845
		 * data processing.
1846
		 *
1847
		 * @since 8.0.0
1848
		 *
1849
		 * @param string $redirect_url Defaults to the site admin URL.
1850
		 */
1851
		$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
1852
1853
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
1854
1855
		/**
1856
		* Filter the URL to redirect the user back to when the authentication process
1857
		* is complete.
1858
		*
1859
		* @since 8.0.0
1860
		*
1861
		* @param string $redirect_url Defaults to the site URL.
1862
		*/
1863
		$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
1864
1865
		$redirect_uri = ( 'calypso' === $data['auth_type'] )
1866
			? $data['redirect_uri']
1867
			: add_query_arg(
1868
				array(
1869
					'action'   => 'authorize',
1870
					'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
1871
					'redirect' => $redirect ? rawurlencode( $redirect ) : false,
1872
				),
1873
				esc_url( $processing_url )
1874
			);
1875
1876
		/**
1877
		 * Filters the token request data.
1878
		 *
1879
		 * @since 8.0.0
1880
		 *
1881
		 * @param array $request_data request data.
1882
		 */
1883
		$body = apply_filters(
1884
			'jetpack_token_request_body',
1885
			array(
1886
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
1887
				'client_secret' => $client_secret->secret,
1888
				'grant_type'    => 'authorization_code',
1889
				'code'          => $data['code'],
1890
				'redirect_uri'  => $redirect_uri,
1891
			)
1892
		);
1893
1894
		$args = array(
1895
			'method'  => 'POST',
1896
			'body'    => $body,
1897
			'headers' => array(
1898
				'Accept' => 'application/json',
1899
			),
1900
		);
1901
1902
		add_filter( 'http_request_timeout', array( $this, 'increase_timeout' ), PHP_INT_MAX - 1 );
1903
		$response = Client::_wp_remote_request( $this->api_url( 'token' ), $args );
1904
		remove_filter( 'http_request_timeout', array( $this, 'increase_timeout' ), PHP_INT_MAX - 1 );
1905
1906
		if ( is_wp_error( $response ) ) {
1907
			return new \WP_Error( 'token_http_request_failed', $response->get_error_message() );
1908
		}
1909
1910
		$code   = wp_remote_retrieve_response_code( $response );
1911
		$entity = wp_remote_retrieve_body( $response );
1912
1913
		if ( $entity ) {
1914
			$json = json_decode( $entity );
1915
		} else {
1916
			$json = false;
1917
		}
1918
1919 View Code Duplication
		if ( 200 !== $code || ! empty( $json->error ) ) {
1920
			if ( empty( $json->error ) ) {
1921
				return new \WP_Error( 'unknown', '', $code );
1922
			}
1923
1924
			/* translators: Error description string. */
1925
			$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : '';
1926
1927
			return new \WP_Error( (string) $json->error, $error_description, $code );
1928
		}
1929
1930
		if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
1931
			return new \WP_Error( 'access_token', '', $code );
1932
		}
1933
1934
		if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
1935
			return new \WP_Error( 'token_type', '', $code );
1936
		}
1937
1938
		if ( empty( $json->scope ) ) {
1939
			return new \WP_Error( 'scope', 'No Scope', $code );
1940
		}
1941
1942
		// TODO: get rid of the error silencer.
1943
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1944
		@list( $role, $hmac ) = explode( ':', $json->scope );
1945
		if ( empty( $role ) || empty( $hmac ) ) {
1946
			return new \WP_Error( 'scope', 'Malformed Scope', $code );
1947
		}
1948
1949
		if ( $this->sign_role( $role ) !== $json->scope ) {
1950
			return new \WP_Error( 'scope', 'Invalid Scope', $code );
1951
		}
1952
1953
		$cap = $roles->translate_role_to_cap( $role );
1954
		if ( ! $cap ) {
1955
			return new \WP_Error( 'scope', 'No Cap', $code );
1956
		}
1957
1958
		if ( ! current_user_can( $cap ) ) {
1959
			return new \WP_Error( 'scope', 'current_user_cannot', $code );
1960
		}
1961
1962
		return (string) $json->access_token;
1963
	}
1964
1965
	/**
1966
	 * Increases the request timeout value to 30 seconds.
1967
	 *
1968
	 * @return int Returns 30.
1969
	 */
1970
	public function increase_timeout() {
1971
		return 30;
1972
	}
1973
1974
	/**
1975
	 * Builds a URL to the Jetpack connection auth page.
1976
	 *
1977
	 * @param WP_User $user (optional) defaults to the current logged in user.
1978
	 * @param String  $redirect (optional) a redirect URL to use instead of the default.
1979
	 * @return string Connect URL.
1980
	 */
1981
	public function get_authorization_url( $user = null, $redirect = null ) {
1982
1983
		if ( empty( $user ) ) {
1984
			$user = wp_get_current_user();
1985
		}
1986
1987
		$roles       = new Roles();
1988
		$role        = $roles->translate_user_to_role( $user );
1989
		$signed_role = $this->sign_role( $role );
1990
1991
		/**
1992
		 * Filter the URL of the first time the user gets redirected back to your site for connection
1993
		 * data processing.
1994
		 *
1995
		 * @since 8.0.0
1996
		 *
1997
		 * @param string $redirect_url Defaults to the site admin URL.
1998
		 */
1999
		$processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) );
2000
2001
		/**
2002
		 * Filter the URL to redirect the user back to when the authorization process
2003
		 * is complete.
2004
		 *
2005
		 * @since 8.0.0
2006
		 *
2007
		 * @param string $redirect_url Defaults to the site URL.
2008
		 */
2009
		$redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect );
2010
2011
		$secrets = $this->generate_secrets( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS );
2012
2013
		/**
2014
		 * Filter the type of authorization.
2015
		 * 'calypso' completes authorization on wordpress.com/jetpack/connect
2016
		 * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com.
2017
		 *
2018
		 * @since 4.3.3
2019
		 *
2020
		 * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'.
2021
		 */
2022
		$auth_type = apply_filters( 'jetpack_auth_type', 'calypso' );
2023
2024
		/**
2025
		 * Filters the user connection request data for additional property addition.
2026
		 *
2027
		 * @since 8.0.0
2028
		 *
2029
		 * @param array $request_data request data.
2030
		 */
2031
		$body = apply_filters(
2032
			'jetpack_connect_request_body',
2033
			array(
2034
				'response_type' => 'code',
2035
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
2036
				'redirect_uri'  => add_query_arg(
2037
					array(
2038
						'action'   => 'authorize',
2039
						'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
2040
						'redirect' => rawurlencode( $redirect ),
2041
					),
2042
					esc_url( $processing_url )
2043
				),
2044
				'state'         => $user->ID,
2045
				'scope'         => $signed_role,
2046
				'user_email'    => $user->user_email,
2047
				'user_login'    => $user->user_login,
2048
				'is_active'     => $this->is_active(),
2049
				'jp_version'    => Constants::get_constant( 'JETPACK__VERSION' ),
2050
				'auth_type'     => $auth_type,
2051
				'secret'        => $secrets['secret_1'],
2052
				'blogname'      => get_option( 'blogname' ),
2053
				'site_url'      => site_url(),
2054
				'home_url'      => home_url(),
2055
				'site_icon'     => get_site_icon_url(),
2056
				'site_lang'     => get_locale(),
2057
				'site_created'  => $this->get_assumed_site_creation_date(),
2058
			)
2059
		);
2060
2061
		$body = $this->apply_activation_source_to_args( urlencode_deep( $body ) );
2062
2063
		$api_url = $this->api_url( 'authorize' );
2064
2065
		return add_query_arg( $body, $api_url );
2066
	}
2067
2068
	/**
2069
	 * Authorizes the user by obtaining and storing the user token.
2070
	 *
2071
	 * @param array $data The request data.
2072
	 * @return string|\WP_Error Returns a string on success.
2073
	 *                          Returns a \WP_Error on failure.
2074
	 */
2075
	public function authorize( $data = array() ) {
2076
		/**
2077
		 * Action fired when user authorization starts.
2078
		 *
2079
		 * @since 8.0.0
2080
		 */
2081
		do_action( 'jetpack_authorize_starting' );
2082
2083
		$roles = new Roles();
2084
		$role  = $roles->translate_current_user_to_role();
2085
2086
		if ( ! $role ) {
2087
			return new \WP_Error( 'no_role', 'Invalid request.', 400 );
2088
		}
2089
2090
		$cap = $roles->translate_role_to_cap( $role );
2091
		if ( ! $cap ) {
2092
			return new \WP_Error( 'no_cap', 'Invalid request.', 400 );
2093
		}
2094
2095
		if ( ! empty( $data['error'] ) ) {
2096
			return new \WP_Error( $data['error'], 'Error included in the request.', 400 );
2097
		}
2098
2099
		if ( ! isset( $data['state'] ) ) {
2100
			return new \WP_Error( 'no_state', 'Request must include state.', 400 );
2101
		}
2102
2103
		if ( ! ctype_digit( $data['state'] ) ) {
2104
			return new \WP_Error( $data['error'], 'State must be an integer.', 400 );
2105
		}
2106
2107
		$current_user_id = get_current_user_id();
2108
		if ( $current_user_id !== (int) $data['state'] ) {
2109
			return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 );
2110
		}
2111
2112
		if ( empty( $data['code'] ) ) {
2113
			return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 );
2114
		}
2115
2116
		$token = $this->get_token( $data );
2117
2118 View Code Duplication
		if ( is_wp_error( $token ) ) {
2119
			$code = $token->get_error_code();
2120
			if ( empty( $code ) ) {
2121
				$code = 'invalid_token';
2122
			}
2123
			return new \WP_Error( $code, $token->get_error_message(), 400 );
2124
		}
2125
2126
		if ( ! $token ) {
2127
			return new \WP_Error( 'no_token', 'Error generating token.', 400 );
2128
		}
2129
2130
		$is_master_user = ! $this->is_active();
2131
2132
		Utils::update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_master_user );
2133
2134
		/**
2135
		 * Fires after user has successfully received an auth token.
2136
		 *
2137
		 * @since 3.9.0
2138
		 */
2139
		do_action( 'jetpack_user_authorized' );
2140
2141
		if ( ! $is_master_user ) {
2142
			/**
2143
			 * Action fired when a secondary user has been authorized.
2144
			 *
2145
			 * @since 8.0.0
2146
			 */
2147
			do_action( 'jetpack_authorize_ending_linked' );
2148
			return 'linked';
2149
		}
2150
2151
		/**
2152
		 * Action fired when the master user has been authorized.
2153
		 *
2154
		 * @since 8.0.0
2155
		 *
2156
		 * @param array $data The request data.
2157
		 */
2158
		do_action( 'jetpack_authorize_ending_authorized', $data );
2159
2160
		\Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' );
2161
2162
		// Start nonce cleaner.
2163
		wp_clear_scheduled_hook( 'jetpack_clean_nonces' );
2164
		wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
2165
2166
		return 'authorized';
2167
	}
2168
2169
	/**
2170
	 * Disconnects from the Jetpack servers.
2171
	 * Forgets all connection details and tells the Jetpack servers to do the same.
2172
	 */
2173
	public function disconnect_site() {
2174
2175
	}
2176
2177
	/**
2178
	 * The Base64 Encoding of the SHA1 Hash of the Input.
2179
	 *
2180
	 * @param string $text The string to hash.
2181
	 * @return string
2182
	 */
2183
	public function sha1_base64( $text ) {
2184
		return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
2185
	}
2186
2187
	/**
2188
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
2189
	 *
2190
	 * @param string $domain The domain to check.
2191
	 *
2192
	 * @return bool|WP_Error
2193
	 */
2194
	public function is_usable_domain( $domain ) {
2195
2196
		// If it's empty, just fail out.
2197
		if ( ! $domain ) {
2198
			return new \WP_Error(
2199
				'fail_domain_empty',
2200
				/* translators: %1$s is a domain name. */
2201
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
2202
			);
2203
		}
2204
2205
		/**
2206
		 * Skips the usuable domain check when connecting a site.
2207
		 *
2208
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
2209
		 *
2210
		 * @since 4.1.0
2211
		 *
2212
		 * @param bool If the check should be skipped. Default false.
2213
		 */
2214
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
2215
			return true;
2216
		}
2217
2218
		// None of the explicit localhosts.
2219
		$forbidden_domains = array(
2220
			'wordpress.com',
2221
			'localhost',
2222
			'localhost.localdomain',
2223
			'127.0.0.1',
2224
			'local.wordpress.test',         // VVV pattern.
2225
			'local.wordpress-trunk.test',   // VVV pattern.
2226
			'src.wordpress-develop.test',   // VVV pattern.
2227
			'build.wordpress-develop.test', // VVV pattern.
2228
		);
2229 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
2230
			return new \WP_Error(
2231
				'fail_domain_forbidden',
2232
				sprintf(
2233
					/* translators: %1$s is a domain name. */
2234
					__(
2235
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
2236
						'jetpack'
2237
					),
2238
					$domain
2239
				)
2240
			);
2241
		}
2242
2243
		// No .test or .local domains.
2244 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
2245
			return new \WP_Error(
2246
				'fail_domain_tld',
2247
				sprintf(
2248
					/* translators: %1$s is a domain name. */
2249
					__(
2250
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
2251
						'jetpack'
2252
					),
2253
					$domain
2254
				)
2255
			);
2256
		}
2257
2258
		// No WPCOM subdomains.
2259 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
2260
			return new \WP_Error(
2261
				'fail_subdomain_wpcom',
2262
				sprintf(
2263
					/* translators: %1$s is a domain name. */
2264
					__(
2265
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
2266
						'jetpack'
2267
					),
2268
					$domain
2269
				)
2270
			);
2271
		}
2272
2273
		// If PHP was compiled without support for the Filter module (very edge case).
2274
		if ( ! function_exists( 'filter_var' ) ) {
2275
			// Just pass back true for now, and let wpcom sort it out.
2276
			return true;
2277
		}
2278
2279
		return true;
2280
	}
2281
2282
	/**
2283
	 * Gets the requested token.
2284
	 *
2285
	 * Tokens are one of two types:
2286
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
2287
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
2288
	 *    are not associated with a user account. They represent the site's connection with
2289
	 *    the Jetpack servers.
2290
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
2291
	 *
2292
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
2293
	 * token, and $private is a secret that should never be displayed anywhere or sent
2294
	 * over the network; it's used only for signing things.
2295
	 *
2296
	 * Blog Tokens can be "Normal" or "Special".
2297
	 * * Normal: The result of a normal connection flow. They look like
2298
	 *   "{$random_string_1}.{$random_string_2}"
2299
	 *   That is, $token_key and $private are both random strings.
2300
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
2301
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
2302
	 *   constant (rare).
2303
	 * * Special: A connection token for sites that have gone through an alternative
2304
	 *   connection flow. They look like:
2305
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
2306
	 *   That is, $private is a random string and $token_key has a special structure with
2307
	 *   lots of semicolons.
2308
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
2309
	 *   JETPACK_BLOG_TOKEN constant.
2310
	 *
2311
	 * In particular, note that Normal Blog Tokens never start with ";" and that
2312
	 * Special Blog Tokens always do.
2313
	 *
2314
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
2315
	 * order:
2316
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
2317
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
2318
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
2319
	 *
2320
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
2321
	 * @param string|false $token_key If provided, check that the token matches the provided input.
2322
	 * @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.
2323
	 *
2324
	 * @return object|false
2325
	 */
2326
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
2327
		$possible_special_tokens = array();
2328
		$possible_normal_tokens  = array();
2329
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
2330
2331
		if ( ( new Status() )->is_no_user_testing_mode() ) {
2332
			$user_tokens = false;
2333
		}
2334
2335
		if ( $user_id ) {
2336
			if ( ! $user_tokens ) {
2337
				return $suppress_errors ? false : new \WP_Error( 'no_user_tokens', __( 'No user tokens found', 'jetpack' ) );
2338
			}
2339
			if ( self::CONNECTION_OWNER === $user_id ) {
2340
				$user_id = \Jetpack_Options::get_option( 'master_user' );
2341
				if ( ! $user_id ) {
2342
					return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option', __( 'No primary user defined', 'jetpack' ) );
2343
				}
2344
			}
2345
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
2346
				// translators: %s is the user ID.
2347
				return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( __( 'No token for user %d', 'jetpack' ), $user_id ) );
2348
			}
2349
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
2350 View Code Duplication
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
2351
				// translators: %s is the user ID.
2352
				return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( __( 'Token for user %d is malformed', 'jetpack' ), $user_id ) );
2353
			}
2354
			if ( $user_token_chunks[2] !== (string) $user_id ) {
2355
				// translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
2356
				return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch', sprintf( __( 'Requesting user_id %1$d does not match token user_id %2$d', 'jetpack' ), $user_id, $user_token_chunks[2] ) );
2357
			}
2358
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
2359
		} else {
2360
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
2361
			if ( $stored_blog_token ) {
2362
				$possible_normal_tokens[] = $stored_blog_token;
2363
			}
2364
2365
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
2366
2367
			if ( $defined_tokens_string ) {
2368
				$defined_tokens = explode( ',', $defined_tokens_string );
2369
				foreach ( $defined_tokens as $defined_token ) {
2370
					if ( ';' === $defined_token[0] ) {
2371
						$possible_special_tokens[] = $defined_token;
2372
					} else {
2373
						$possible_normal_tokens[] = $defined_token;
2374
					}
2375
				}
2376
			}
2377
		}
2378
2379
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
2380
			$possible_tokens = $possible_normal_tokens;
2381
		} else {
2382
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
2383
		}
2384
2385
		if ( ! $possible_tokens ) {
2386
			// If no user tokens were found, it would have failed earlier, so this is about blog token.
2387
			return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens', __( 'No blog token found', 'jetpack' ) );
2388
		}
2389
2390
		$valid_token = false;
2391
2392
		if ( false === $token_key ) {
2393
			// Use first token.
2394
			$valid_token = $possible_tokens[0];
2395
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
2396
			// Use first normal token.
2397
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
2398
		} else {
2399
			// Use the token matching $token_key or false if none.
2400
			// Ensure we check the full key.
2401
			$token_check = rtrim( $token_key, '.' ) . '.';
2402
2403
			foreach ( $possible_tokens as $possible_token ) {
2404
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
2405
					$valid_token = $possible_token;
2406
					break;
2407
				}
2408
			}
2409
		}
2410
2411
		if ( ! $valid_token ) {
2412
			if ( $user_id ) {
2413
				// translators: %d is the user ID.
2414
				return $suppress_errors ? false : new \WP_Error( 'no_valid_user_token', sprintf( __( 'Invalid token for user %d', 'jetpack' ), $user_id ) );
2415
			} else {
2416
				return $suppress_errors ? false : new \WP_Error( 'no_valid_blog_token', __( 'Invalid blog token', 'jetpack' ) );
2417
			}
2418
		}
2419
2420
		return (object) array(
2421
			'secret'           => $valid_token,
2422
			'external_user_id' => (int) $user_id,
2423
		);
2424
	}
2425
2426
	/**
2427
	 * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths
2428
	 * since it is passed by reference to various methods.
2429
	 * Capture it here so we can verify the signature later.
2430
	 *
2431
	 * @param array $methods an array of available XMLRPC methods.
2432
	 * @return array the same array, since this method doesn't add or remove anything.
2433
	 */
2434
	public function xmlrpc_methods( $methods ) {
2435
		$this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA'];
2436
		return $methods;
2437
	}
2438
2439
	/**
2440
	 * Resets the raw post data parameter for testing purposes.
2441
	 */
2442
	public function reset_raw_post_data() {
2443
		$this->raw_post_data = null;
2444
	}
2445
2446
	/**
2447
	 * Registering an additional method.
2448
	 *
2449
	 * @param array $methods an array of available XMLRPC methods.
2450
	 * @return array the amended array in case the method is added.
2451
	 */
2452
	public function public_xmlrpc_methods( $methods ) {
2453
		if ( array_key_exists( 'wp.getOptions', $methods ) ) {
2454
			$methods['wp.getOptions'] = array( $this, 'jetpack_get_options' );
2455
		}
2456
		return $methods;
2457
	}
2458
2459
	/**
2460
	 * Handles a getOptions XMLRPC method call.
2461
	 *
2462
	 * @param array $args method call arguments.
2463
	 * @return an amended XMLRPC server options array.
2464
	 */
2465
	public function jetpack_get_options( $args ) {
2466
		global $wp_xmlrpc_server;
2467
2468
		$wp_xmlrpc_server->escape( $args );
2469
2470
		$username = $args[1];
2471
		$password = $args[2];
2472
2473
		$user = $wp_xmlrpc_server->login( $username, $password );
2474
		if ( ! $user ) {
2475
			return $wp_xmlrpc_server->error;
2476
		}
2477
2478
		$options   = array();
2479
		$user_data = $this->get_connected_user_data();
2480
		if ( is_array( $user_data ) ) {
2481
			$options['jetpack_user_id']         = array(
2482
				'desc'     => __( 'The WP.com user ID of the connected user', 'jetpack' ),
2483
				'readonly' => true,
2484
				'value'    => $user_data['ID'],
2485
			);
2486
			$options['jetpack_user_login']      = array(
2487
				'desc'     => __( 'The WP.com username of the connected user', 'jetpack' ),
2488
				'readonly' => true,
2489
				'value'    => $user_data['login'],
2490
			);
2491
			$options['jetpack_user_email']      = array(
2492
				'desc'     => __( 'The WP.com user email of the connected user', 'jetpack' ),
2493
				'readonly' => true,
2494
				'value'    => $user_data['email'],
2495
			);
2496
			$options['jetpack_user_site_count'] = array(
2497
				'desc'     => __( 'The number of sites of the connected WP.com user', 'jetpack' ),
2498
				'readonly' => true,
2499
				'value'    => $user_data['site_count'],
2500
			);
2501
		}
2502
		$wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options );
2503
		$args                           = stripslashes_deep( $args );
2504
		return $wp_xmlrpc_server->wp_getOptions( $args );
2505
	}
2506
2507
	/**
2508
	 * Adds Jetpack-specific options to the output of the XMLRPC options method.
2509
	 *
2510
	 * @param array $options standard Core options.
2511
	 * @return array amended options.
2512
	 */
2513
	public function xmlrpc_options( $options ) {
2514
		$jetpack_client_id = false;
2515
		if ( $this->is_active() ) {
2516
			$jetpack_client_id = \Jetpack_Options::get_option( 'id' );
2517
		}
2518
		$options['jetpack_version'] = array(
2519
			'desc'     => __( 'Jetpack Plugin Version', 'jetpack' ),
2520
			'readonly' => true,
2521
			'value'    => Constants::get_constant( 'JETPACK__VERSION' ),
2522
		);
2523
2524
		$options['jetpack_client_id'] = array(
2525
			'desc'     => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack' ),
2526
			'readonly' => true,
2527
			'value'    => $jetpack_client_id,
2528
		);
2529
		return $options;
2530
	}
2531
2532
	/**
2533
	 * Resets the saved authentication state in between testing requests.
2534
	 */
2535
	public function reset_saved_auth_state() {
2536
		$this->xmlrpc_verification = null;
2537
	}
2538
2539
	/**
2540
	 * Sign a user role with the master access token.
2541
	 * If not specified, will default to the current user.
2542
	 *
2543
	 * @access public
2544
	 *
2545
	 * @param string $role    User role.
2546
	 * @param int    $user_id ID of the user.
2547
	 * @return string Signed user role.
2548
	 */
2549
	public function sign_role( $role, $user_id = null ) {
2550
		if ( empty( $user_id ) ) {
2551
			$user_id = (int) get_current_user_id();
2552
		}
2553
2554
		if ( ! $user_id ) {
2555
			return false;
2556
		}
2557
2558
		$token = $this->get_access_token();
2559
		if ( ! $token || is_wp_error( $token ) ) {
2560
			return false;
2561
		}
2562
2563
		return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
2564
	}
2565
2566
	/**
2567
	 * Set the plugin instance.
2568
	 *
2569
	 * @param Plugin $plugin_instance The plugin instance.
2570
	 *
2571
	 * @return $this
2572
	 */
2573
	public function set_plugin_instance( Plugin $plugin_instance ) {
2574
		$this->plugin = $plugin_instance;
2575
2576
		return $this;
2577
	}
2578
2579
	/**
2580
	 * Retrieve the plugin management object.
2581
	 *
2582
	 * @return Plugin
2583
	 */
2584
	public function get_plugin() {
2585
		return $this->plugin;
2586
	}
2587
2588
	/**
2589
	 * Get all connected plugins information, excluding those disconnected by user.
2590
	 * WARNING: the method cannot be called until Plugin_Storage::configure is called, which happens on plugins_loaded
2591
	 * Even if you don't use Jetpack Config, it may be introduced later by other plugins,
2592
	 * so please make sure not to run the method too early in the code.
2593
	 *
2594
	 * @return array|WP_Error
2595
	 */
2596
	public function get_connected_plugins() {
2597
		$maybe_plugins = Plugin_Storage::get_all( true );
2598
2599
		if ( $maybe_plugins instanceof WP_Error ) {
2600
			return $maybe_plugins;
2601
		}
2602
2603
		return $maybe_plugins;
2604
	}
2605
2606
	/**
2607
	 * Force plugin disconnect. After its called, the plugin will not be allowed to use the connection.
2608
	 * Note: this method does not remove any access tokens.
2609
	 *
2610
	 * @return bool
2611
	 */
2612
	public function disable_plugin() {
2613
		if ( ! $this->plugin ) {
2614
			return false;
2615
		}
2616
2617
		return $this->plugin->disable();
2618
	}
2619
2620
	/**
2621
	 * Force plugin reconnect after user-initiated disconnect.
2622
	 * After its called, the plugin will be allowed to use the connection again.
2623
	 * Note: this method does not initialize access tokens.
2624
	 *
2625
	 * @return bool
2626
	 */
2627
	public function enable_plugin() {
2628
		if ( ! $this->plugin ) {
2629
			return false;
2630
		}
2631
2632
		return $this->plugin->enable();
2633
	}
2634
2635
	/**
2636
	 * Whether the plugin is allowed to use the connection, or it's been disconnected by user.
2637
	 * If no plugin slug was passed into the constructor, always returns true.
2638
	 *
2639
	 * @return bool
2640
	 */
2641
	public function is_plugin_enabled() {
2642
		if ( ! $this->plugin ) {
2643
			return true;
2644
		}
2645
2646
		return $this->plugin->is_enabled();
2647
	}
2648
2649
	/**
2650
	 * Perform the API request to refresh the blog token.
2651
	 * Note that we are making this request on behalf of the Jetpack master user,
2652
	 * given they were (most probably) the ones that registered the site at the first place.
2653
	 *
2654
	 * @return WP_Error|bool The result of updating the blog_token option.
2655
	 */
2656
	public static function refresh_blog_token() {
2657
		( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' );
2658
2659
		$blog_id = Jetpack_Options::get_option( 'id' );
2660
		if ( ! $blog_id ) {
2661
			return new WP_Error( 'site_not_registered', 'Site not registered.' );
2662
		}
2663
2664
		$url     = sprintf(
2665
			'%s/%s/v%s/%s',
2666
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
2667
			'wpcom',
2668
			'2',
2669
			'sites/' . $blog_id . '/jetpack-refresh-blog-token'
2670
		);
2671
		$method  = 'POST';
2672
		$user_id = get_current_user_id();
2673
2674
		$response = Client::remote_request( compact( 'url', 'method', 'user_id' ) );
2675
2676
		if ( is_wp_error( $response ) ) {
2677
			return new WP_Error( 'refresh_blog_token_http_request_failed', $response->get_error_message() );
2678
		}
2679
2680
		$code   = wp_remote_retrieve_response_code( $response );
2681
		$entity = wp_remote_retrieve_body( $response );
2682
2683
		if ( $entity ) {
2684
			$json = json_decode( $entity );
2685
		} else {
2686
			$json = false;
2687
		}
2688
2689 View Code Duplication
		if ( 200 !== $code ) {
2690
			if ( empty( $json->code ) ) {
2691
				return new WP_Error( 'unknown', '', $code );
2692
			}
2693
2694
			/* translators: Error description string. */
2695
			$error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->message ) : '';
2696
2697
			return new WP_Error( (string) $json->code, $error_description, $code );
2698
		}
2699
2700
		if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) {
2701
			return new WP_Error( 'jetpack_secret', '', $code );
2702
		}
2703
2704
		return Jetpack_Options::update_option( 'blog_token', (string) $json->jetpack_secret );
2705
	}
2706
2707
	/**
2708
	 * Disconnect the user from WP.com, and initiate the reconnect process.
2709
	 *
2710
	 * @return bool
2711
	 */
2712
	public static function refresh_user_token() {
2713
		( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' );
2714
2715
		self::disconnect_user( null, true );
2716
2717
		return true;
2718
	}
2719
2720
	/**
2721
	 * Fetches a signed token.
2722
	 *
2723
	 * @param object $token the token.
2724
	 * @return WP_Error|string a signed token
2725
	 */
2726
	public function get_signed_token( $token ) {
2727
		if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
2728
			return new WP_Error( 'invalid_token' );
2729
		}
2730
2731
		list( $token_key, $token_secret ) = explode( '.', $token->secret );
2732
2733
		$token_key = sprintf(
2734
			'%s:%d:%d',
2735
			$token_key,
2736
			Constants::get_constant( 'JETPACK__API_VERSION' ),
2737
			$token->external_user_id
2738
		);
2739
2740
		$timestamp = time();
2741
2742 View Code Duplication
		if ( function_exists( 'wp_generate_password' ) ) {
2743
			$nonce = wp_generate_password( 10, false );
2744
		} else {
2745
			$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
2746
		}
2747
2748
		$normalized_request_string = join(
2749
			"\n",
2750
			array(
2751
				$token_key,
2752
				$timestamp,
2753
				$nonce,
2754
			)
2755
		) . "\n";
2756
2757
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
2758
		$signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
2759
2760
		$auth = array(
2761
			'token'     => $token_key,
2762
			'timestamp' => $timestamp,
2763
			'nonce'     => $nonce,
2764
			'signature' => $signature,
2765
		);
2766
2767
		$header_pieces = array();
2768
		foreach ( $auth as $key => $value ) {
2769
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
2770
		}
2771
2772
		return join( ' ', $header_pieces );
2773
	}
2774
2775
	/**
2776
	 * If connection is active, add the list of plugins using connection to the heartbeat (except Jetpack itself)
2777
	 *
2778
	 * @param array $stats The Heartbeat stats array.
2779
	 * @return array $stats
2780
	 */
2781
	public function add_stats_to_heartbeat( $stats ) {
2782
2783
		if ( ! $this->is_active() ) {
2784
			return $stats;
2785
		}
2786
2787
		$active_plugins_using_connection = Plugin_Storage::get_all();
2788
		foreach ( array_keys( $active_plugins_using_connection ) as $plugin_slug ) {
2789
			if ( 'jetpack' !== $plugin_slug ) {
2790
				$stats_group             = isset( $active_plugins_using_connection['jetpack'] ) ? 'combined-connection' : 'standalone-connection';
2791
				$stats[ $stats_group ][] = $plugin_slug;
2792
			}
2793
		}
2794
		return $stats;
2795
	}
2796
2797
}
2798