Completed
Push — fix/15025-map-block-infinite-s... ( 9a8b21...52d6c3 )
by
unknown
63:16 queued 56:01
created

packages/connection/src/class-manager.php (2 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\Roles;
12
use Automattic\Jetpack\Tracking;
13
14
/**
15
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
16
 * and Jetpack.
17
 */
18
class Manager {
19
20
	const SECRETS_MISSING        = 'secrets_missing';
21
	const SECRETS_EXPIRED        = 'secrets_expired';
22
	const SECRETS_OPTION_NAME    = 'jetpack_secrets';
23
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
24
	const JETPACK_MASTER_USER    = true;
25
26
	/**
27
	 * The procedure that should be run to generate secrets.
28
	 *
29
	 * @var Callable
30
	 */
31
	protected $secret_callable;
32
33
	/**
34
	 * A copy of the raw POST data for signature verification purposes.
35
	 *
36
	 * @var String
37
	 */
38
	protected $raw_post_data;
39
40
	/**
41
	 * Verification data needs to be stored to properly verify everything.
42
	 *
43
	 * @var Object
44
	 */
45
	private $xmlrpc_verification = null;
46
47
	/**
48
	 * Plugin management object.
49
	 *
50
	 * @var Plugin
51
	 */
52
	private $plugin = null;
53
54
	/**
55
	 * Initialize the object.
56
	 * Make sure to call the "Configure" first.
57
	 *
58
	 * @param string $plugin_slug Slug of the plugin using the connection (optional, but encouraged).
0 ignored issues
show
Should the type for parameter $plugin_slug not be string|null?

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...
59
	 *
60
	 * @see \Automattic\Jetpack\Config
61
	 */
62
	public function __construct( $plugin_slug = null ) {
63
		if ( $plugin_slug && is_string( $plugin_slug ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugin_slug of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
64
			$this->set_plugin_instance( new Plugin( $plugin_slug ) );
65
		}
66
	}
67
68
	/**
69
	 * Initializes required listeners. This is done separately from the constructors
70
	 * because some objects sometimes need to instantiate separate objects of this class.
71
	 *
72
	 * @todo Implement a proper nonce verification.
73
	 */
74
	public static function configure() {
75
		$manager = new self();
76
77
		$manager->setup_xmlrpc_handlers(
78
			$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
79
			$manager->is_active(),
80
			$manager->verify_xml_rpc_signature()
81
		);
82
83
		if ( $manager->is_active() ) {
84
			add_filter( 'xmlrpc_methods', array( $manager, 'public_xmlrpc_methods' ) );
85
		} else {
86
			add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ) );
87
		}
88
89
		add_action( 'jetpack_clean_nonces', array( $manager, 'clean_nonces' ) );
90
		if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) {
91
			wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
92
		}
93
94
		add_filter(
95
			'jetpack_constant_default_value',
96
			__NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
97
			10,
98
			2
99
		);
100
	}
101
102
	/**
103
	 * Sets up the XMLRPC request handlers.
104
	 *
105
	 * @param array                  $request_params incoming request parameters.
106
	 * @param Boolean                $is_active whether the connection is currently active.
107
	 * @param Boolean                $is_signed whether the signature check has been successful.
108
	 * @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
109
	 */
110
	public function setup_xmlrpc_handlers(
111
		$request_params,
112
		$is_active,
113
		$is_signed,
114
		\Jetpack_XMLRPC_Server $xmlrpc_server = null
115
	) {
116
		add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 );
117
118
		if (
119
			! isset( $request_params['for'] )
120
			|| 'jetpack' !== $request_params['for']
121
		) {
122
			return false;
123
		}
124
125
		// Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
126
		if (
127
			isset( $request_params['jetpack'] )
128
			&& 'comms' === $request_params['jetpack']
129
		) {
130
			if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
131
				// Use the real constant here for WordPress' sake.
132
				define( 'XMLRPC_REQUEST', true );
133
			}
134
135
			add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) );
136
137
			add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 );
138
		}
139
140
		if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
141
			return false;
142
		}
143
		// Display errors can cause the XML to be not well formed.
144
		@ini_set( 'display_errors', false ); // phpcs:ignore
145
146
		if ( $xmlrpc_server ) {
147
			$this->xmlrpc_server = $xmlrpc_server;
148
		} else {
149
			$this->xmlrpc_server = new \Jetpack_XMLRPC_Server();
150
		}
151
152
		$this->require_jetpack_authentication();
153
154
		if ( $is_active ) {
155
			// Hack to preserve $HTTP_RAW_POST_DATA.
156
			add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
157
158
			if ( $is_signed ) {
159
				// The actual API methods.
160
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) );
161
			} else {
162
				// The jetpack.authorize method should be available for unauthenticated users on a site with an
163
				// active Jetpack connection, so that additional users can link their account.
164
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) );
165
			}
166
		} else {
167
			// The bootstrap API methods.
168
			add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) );
169
170
			if ( $is_signed ) {
171
				// The jetpack Provision method is available for blog-token-signed requests.
172
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) );
173
			} else {
174
				new XMLRPC_Connector( $this );
175
			}
176
		}
177
178
		// Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on.
179
		add_filter( 'pre_option_enable_xmlrpc', '__return_true' );
180
		return true;
181
	}
182
183
	/**
184
	 * Initializes the REST API connector on the init hook.
185
	 */
186
	public function initialize_rest_api_registration_connector() {
187
		new REST_Connector( $this );
188
	}
189
190
	/**
191
	 * Since a lot of hosts use a hammer approach to "protecting" WordPress sites,
192
	 * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive
193
	 * security/firewall policies, we provide our own alternate XML RPC API endpoint
194
	 * which is accessible via a different URI. Most of the below is copied directly
195
	 * from /xmlrpc.php so that we're replicating it as closely as possible.
196
	 *
197
	 * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
198
	 */
199
	public function alternate_xmlrpc() {
200
		// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
201
		// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
202
		global $HTTP_RAW_POST_DATA;
203
204
		// Some browser-embedded clients send cookies. We don't want them.
205
		$_COOKIE = array();
206
207
		// A fix for mozBlog and other cases where '<?xml' isn't on the very first line.
208
		if ( isset( $HTTP_RAW_POST_DATA ) ) {
209
			$HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA );
210
		}
211
212
		// phpcs:enable
213
214
		include_once ABSPATH . 'wp-admin/includes/admin.php';
215
		include_once ABSPATH . WPINC . '/class-IXR.php';
216
		include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php';
217
218
		/**
219
		 * Filters the class used for handling XML-RPC requests.
220
		 *
221
		 * @since 3.1.0
222
		 *
223
		 * @param string $class The name of the XML-RPC server class.
224
		 */
225
		$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' );
226
		$wp_xmlrpc_server       = new $wp_xmlrpc_server_class();
227
228
		// Fire off the request.
229
		nocache_headers();
230
		$wp_xmlrpc_server->serve_request();
231
232
		exit;
233
	}
234
235
	/**
236
	 * Removes all XML-RPC methods that are not `jetpack.*`.
237
	 * Only used in our alternate XML-RPC endpoint, where we want to
238
	 * ensure that Core and other plugins' methods are not exposed.
239
	 *
240
	 * @param array $methods a list of registered WordPress XMLRPC methods.
241
	 * @return array filtered $methods
242
	 */
243
	public function remove_non_jetpack_xmlrpc_methods( $methods ) {
244
		$jetpack_methods = array();
245
246
		foreach ( $methods as $method => $callback ) {
247
			if ( 0 === strpos( $method, 'jetpack.' ) ) {
248
				$jetpack_methods[ $method ] = $callback;
249
			}
250
		}
251
252
		return $jetpack_methods;
253
	}
254
255
	/**
256
	 * Removes all other authentication methods not to allow other
257
	 * methods to validate unauthenticated requests.
258
	 */
259
	public function require_jetpack_authentication() {
260
		// Don't let anyone authenticate.
261
		$_COOKIE = array();
262
		remove_all_filters( 'authenticate' );
263
		remove_all_actions( 'wp_login_failed' );
264
265
		if ( $this->is_active() ) {
266
			// Allow Jetpack authentication.
267
			add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 );
268
		}
269
	}
270
271
	/**
272
	 * Authenticates XML-RPC and other requests from the Jetpack Server
273
	 *
274
	 * @param WP_User|Mixed $user user object if authenticated.
275
	 * @param String        $username username.
276
	 * @param String        $password password string.
277
	 * @return WP_User|Mixed authenticated user or error.
278
	 */
279
	public function authenticate_jetpack( $user, $username, $password ) {
280
		if ( is_a( $user, '\\WP_User' ) ) {
281
			return $user;
282
		}
283
284
		$token_details = $this->verify_xml_rpc_signature();
285
286
		if ( ! $token_details ) {
287
			return $user;
288
		}
289
290
		if ( 'user' !== $token_details['type'] ) {
291
			return $user;
292
		}
293
294
		if ( ! $token_details['user_id'] ) {
295
			return $user;
296
		}
297
298
		nocache_headers();
299
300
		return new \WP_User( $token_details['user_id'] );
301
	}
302
303
	/**
304
	 * Verifies the signature of the current request.
305
	 *
306
	 * @return false|array
307
	 */
308
	public function verify_xml_rpc_signature() {
309
		if ( is_null( $this->xmlrpc_verification ) ) {
310
			$this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature();
311
312
			if ( is_wp_error( $this->xmlrpc_verification ) ) {
313
				/**
314
				 * Action for logging XMLRPC signature verification errors. This data is sensitive.
315
				 *
316
				 * Error codes:
317
				 * - malformed_token
318
				 * - malformed_user_id
319
				 * - unknown_token
320
				 * - could_not_sign
321
				 * - invalid_nonce
322
				 * - signature_mismatch
323
				 *
324
				 * @since 7.5.0
325
				 *
326
				 * @param WP_Error $signature_verification_error The verification error
327
				 */
328
				do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification );
329
			}
330
		}
331
332
		return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification;
333
	}
334
335
	/**
336
	 * Verifies the signature of the current request.
337
	 *
338
	 * This function has side effects and should not be used. Instead,
339
	 * use the memoized version `->verify_xml_rpc_signature()`.
340
	 *
341
	 * @internal
342
	 * @todo Refactor to use proper nonce verification.
343
	 */
344
	private function internal_verify_xml_rpc_signature() {
345
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
346
		// It's not for us.
347
		if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) {
348
			return false;
349
		}
350
351
		$signature_details = array(
352
			'token'     => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '',
353
			'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '',
354
			'nonce'     => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '',
355
			'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '',
356
			'method'    => wp_unslash( $_SERVER['REQUEST_METHOD'] ),
357
			'url'       => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later.
358
			'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '',
359
		);
360
361
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
362
		@list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) );
363
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
364
365
		$jetpack_api_version = Constants::get_constant( 'JETPACK__API_VERSION' );
366
367
		if (
368
			empty( $token_key )
369
		||
370
			empty( $version ) || strval( $jetpack_api_version ) !== $version ) {
371
			return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) );
372
		}
373
374
		if ( '0' === $user_id ) {
375
			$token_type = 'blog';
376
			$user_id    = 0;
377
		} else {
378
			$token_type = 'user';
379
			if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) {
380
				return new \WP_Error(
381
					'malformed_user_id',
382
					'Malformed user_id in request',
383
					compact( 'signature_details' )
384
				);
385
			}
386
			$user_id = (int) $user_id;
387
388
			$user = new \WP_User( $user_id );
389
			if ( ! $user || ! $user->exists() ) {
390
				return new \WP_Error(
391
					'unknown_user',
392
					sprintf( 'User %d does not exist', $user_id ),
393
					compact( 'signature_details' )
394
				);
395
			}
396
		}
397
398
		$token = $this->get_access_token( $user_id, $token_key, false );
399
		if ( is_wp_error( $token ) ) {
400
			$token->add_data( compact( 'signature_details' ) );
401
			return $token;
402
		} elseif ( ! $token ) {
403
			return new \WP_Error(
404
				'unknown_token',
405
				sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ),
406
				compact( 'signature_details' )
407
			);
408
		}
409
410
		$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
411
		// phpcs:disable WordPress.Security.NonceVerification.Missing
412
		if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
413
			$post_data   = $_POST;
414
			$file_hashes = array();
415
			foreach ( $post_data as $post_data_key => $post_data_value ) {
416
				if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) {
417
					continue;
418
				}
419
				$post_data_key                 = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
420
				$file_hashes[ $post_data_key ] = $post_data_value;
421
			}
422
423
			foreach ( $file_hashes as $post_data_key => $post_data_value ) {
424
				unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] );
425
				$post_data[ $post_data_key ] = $post_data_value;
426
			}
427
428
			ksort( $post_data );
429
430
			$body = http_build_query( stripslashes_deep( $post_data ) );
431
		} elseif ( is_null( $this->raw_post_data ) ) {
432
			$body = file_get_contents( 'php://input' );
433
		} else {
434
			$body = null;
435
		}
436
		// phpcs:enable
437
438
		$signature = $jetpack_signature->sign_current_request(
439
			array( 'body' => is_null( $body ) ? $this->raw_post_data : $body )
440
		);
441
442
		$signature_details['url'] = $jetpack_signature->current_request_url;
443
444
		if ( ! $signature ) {
445
			return new \WP_Error(
446
				'could_not_sign',
447
				'Unknown signature error',
448
				compact( 'signature_details' )
449
			);
450
		} elseif ( is_wp_error( $signature ) ) {
451
			return $signature;
452
		}
453
454
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
455
		$timestamp = (int) $_GET['timestamp'];
456
		$nonce     = stripslashes( (string) $_GET['nonce'] );
457
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
458
459
		// Use up the nonce regardless of whether the signature matches.
460
		if ( ! $this->add_nonce( $timestamp, $nonce ) ) {
461
			return new \WP_Error(
462
				'invalid_nonce',
463
				'Could not add nonce',
464
				compact( 'signature_details' )
465
			);
466
		}
467
468
		// Be careful about what you do with this debugging data.
469
		// If a malicious requester has access to the expected signature,
470
		// bad things might be possible.
471
		$signature_details['expected'] = $signature;
472
473
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
474
		if ( ! hash_equals( $signature, $_GET['signature'] ) ) {
475
			return new \WP_Error(
476
				'signature_mismatch',
477
				'Signature mismatch',
478
				compact( 'signature_details' )
479
			);
480
		}
481
482
		/**
483
		 * Action for additional token checking.
484
		 *
485
		 * @since 7.7.0
486
		 *
487
		 * @param array $post_data request data.
488
		 * @param array $token_data token data.
489
		 */
490
		return apply_filters(
491
			'jetpack_signature_check_token',
492
			array(
493
				'type'      => $token_type,
494
				'token_key' => $token_key,
495
				'user_id'   => $token->external_user_id,
496
			),
497
			$token,
498
			$this->raw_post_data
499
		);
500
	}
501
502
	/**
503
	 * Returns true if the current site is connected to WordPress.com.
504
	 *
505
	 * @return Boolean is the site connected?
506
	 */
507
	public function is_active() {
508
		return (bool) $this->get_access_token( self::JETPACK_MASTER_USER );
509
	}
510
511
	/**
512
	 * Returns true if the site has both a token and a blog id, which indicates a site has been registered.
513
	 *
514
	 * @access public
515
	 *
516
	 * @return bool
517
	 */
518
	public function is_registered() {
519
		$blog_id   = \Jetpack_Options::get_option( 'id' );
520
		$has_token = $this->is_active();
521
		return $blog_id && $has_token;
522
	}
523
524
	/**
525
	 * Checks to see if the connection owner of the site is missing.
526
	 *
527
	 * @return bool
528
	 */
529
	public function is_missing_connection_owner() {
530
		$connection_owner = $this->get_connection_owner_id();
531
		if ( ! get_user_by( 'id', $connection_owner ) ) {
532
			return true;
533
		}
534
535
		return false;
536
	}
537
538
	/**
539
	 * Returns true if the user with the specified identifier is connected to
540
	 * WordPress.com.
541
	 *
542
	 * @param Integer|Boolean $user_id the user identifier.
543
	 * @return Boolean is the user connected?
544
	 */
545
	public function is_user_connected( $user_id = false ) {
546
		$user_id = false === $user_id ? get_current_user_id() : absint( $user_id );
547
		if ( ! $user_id ) {
548
			return false;
549
		}
550
551
		return (bool) $this->get_access_token( $user_id );
552
	}
553
554
	/**
555
	 * Returns the local user ID of the connection owner.
556
	 *
557
	 * @return string|int Returns the ID of the connection owner or False if no connection owner found.
558
	 */
559 View Code Duplication
	public function get_connection_owner_id() {
560
		$user_token       = $this->get_access_token( self::JETPACK_MASTER_USER );
561
		$connection_owner = false;
562
		if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) {
563
			$connection_owner = $user_token->external_user_id;
564
		}
565
566
		return $connection_owner;
567
	}
568
569
	/**
570
	 * Returns an array of user_id's that have user tokens for communicating with wpcom.
571
	 * Able to select by specific capability.
572
	 *
573
	 * @param string $capability The capability of the user.
574
	 * @return array Array of WP_User objects if found.
575
	 */
576
	public function get_connected_users( $capability = 'any' ) {
577
		$connected_users    = array();
578
		$connected_user_ids = array_keys( \Jetpack_Options::get_option( 'user_tokens' ) );
579
580
		if ( ! empty( $connected_user_ids ) ) {
581
			foreach ( $connected_user_ids as $id ) {
582
				// Check for capability.
583
				if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
584
					continue;
585
				}
586
587
				$connected_users[] = get_userdata( $id );
588
			}
589
		}
590
591
		return $connected_users;
592
	}
593
594
	/**
595
	 * Get the wpcom user data of the current|specified connected user.
596
	 *
597
	 * @todo Refactor to properly load the XMLRPC client independently.
598
	 *
599
	 * @param Integer $user_id the user identifier.
600
	 * @return Object the user object.
601
	 */
602 View Code Duplication
	public function get_connected_user_data( $user_id = null ) {
603
		if ( ! $user_id ) {
604
			$user_id = get_current_user_id();
605
		}
606
607
		$transient_key    = "jetpack_connected_user_data_$user_id";
608
		$cached_user_data = get_transient( $transient_key );
609
610
		if ( $cached_user_data ) {
611
			return $cached_user_data;
612
		}
613
614
		$xml = new \Jetpack_IXR_Client(
615
			array(
616
				'user_id' => $user_id,
617
			)
618
		);
619
		$xml->query( 'wpcom.getUser' );
620
		if ( ! $xml->isError() ) {
621
			$user_data = $xml->getResponse();
622
			set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS );
623
			return $user_data;
624
		}
625
626
		return false;
627
	}
628
629
	/**
630
	 * Returns a user object of the connection owner.
631
	 *
632
	 * @return object|false False if no connection owner found.
633
	 */
634 View Code Duplication
	public function get_connection_owner() {
635
		$user_token = $this->get_access_token( self::JETPACK_MASTER_USER );
636
637
		$connection_owner = false;
638
		if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) {
639
			$connection_owner = get_userdata( $user_token->external_user_id );
640
		}
641
642
		return $connection_owner;
643
	}
644
645
	/**
646
	 * Returns true if the provided user is the Jetpack connection owner.
647
	 * If user ID is not specified, the current user will be used.
648
	 *
649
	 * @param Integer|Boolean $user_id the user identifier. False for current user.
650
	 * @return Boolean True the user the connection owner, false otherwise.
651
	 */
652 View Code Duplication
	public function is_connection_owner( $user_id = false ) {
653
		if ( ! $user_id ) {
654
			$user_id = get_current_user_id();
655
		}
656
657
		$user_token = $this->get_access_token( self::JETPACK_MASTER_USER );
658
659
		return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id;
660
	}
661
662
	/**
663
	 * Connects the user with a specified ID to a WordPress.com user using the
664
	 * remote login flow.
665
	 *
666
	 * @access public
667
	 *
668
	 * @param Integer $user_id (optional) the user identifier, defaults to current user.
669
	 * @param String  $redirect_url the URL to redirect the user to for processing, defaults to
670
	 *                              admin_url().
671
	 * @return WP_Error only in case of a failed user lookup.
672
	 */
673
	public function connect_user( $user_id = null, $redirect_url = null ) {
674
		$user = null;
675
		if ( null === $user_id ) {
676
			$user = wp_get_current_user();
677
		} else {
678
			$user = get_user_by( 'ID', $user_id );
679
		}
680
681
		if ( empty( $user ) ) {
682
			return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' );
683
		}
684
685
		if ( null === $redirect_url ) {
686
			$redirect_url = admin_url();
687
		}
688
689
		// Using wp_redirect intentionally because we're redirecting outside.
690
		wp_redirect( $this->get_authorization_url( $user ) ); // phpcs:ignore WordPress.Security.SafeRedirect
691
		exit();
692
	}
693
694
	/**
695
	 * Unlinks the current user from the linked WordPress.com user.
696
	 *
697
	 * @access public
698
	 * @static
699
	 *
700
	 * @todo Refactor to properly load the XMLRPC client independently.
701
	 *
702
	 * @param Integer $user_id the user identifier.
703
	 * @return Boolean Whether the disconnection of the user was successful.
704
	 */
705
	public static function disconnect_user( $user_id = null ) {
706
		$tokens = \Jetpack_Options::get_option( 'user_tokens' );
707
		if ( ! $tokens ) {
708
			return false;
709
		}
710
711
		$user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id );
712
713
		if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) {
714
			return false;
715
		}
716
717
		if ( ! isset( $tokens[ $user_id ] ) ) {
718
			return false;
719
		}
720
721
		$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) );
722
		$xml->query( 'jetpack.unlink_user', $user_id );
723
724
		unset( $tokens[ $user_id ] );
725
726
		\Jetpack_Options::update_option( 'user_tokens', $tokens );
727
728
		/**
729
		 * Fires after the current user has been unlinked from WordPress.com.
730
		 *
731
		 * @since 4.1.0
732
		 *
733
		 * @param int $user_id The current user's ID.
734
		 */
735
		do_action( 'jetpack_unlinked_user', $user_id );
736
737
		return true;
738
	}
739
740
	/**
741
	 * Returns the requested Jetpack API URL.
742
	 *
743
	 * @param String $relative_url the relative API path.
744
	 * @return String API URL.
745
	 */
746
	public function api_url( $relative_url ) {
747
		$api_base    = Constants::get_constant( 'JETPACK__API_BASE' );
748
		$api_version = '/' . Constants::get_constant( 'JETPACK__API_VERSION' ) . '/';
749
750
		/**
751
		 * Filters whether the connection manager should use the iframe authorization
752
		 * flow instead of the regular redirect-based flow.
753
		 *
754
		 * @since 8.3.0
755
		 *
756
		 * @param Boolean $is_iframe_flow_used should the iframe flow be used, defaults to false.
757
		 */
758
		$iframe_flow = apply_filters( 'jetpack_use_iframe_authorization_flow', false );
759
760
		// Do not modify anything that is not related to authorize requests.
761
		if ( 'authorize' === $relative_url && $iframe_flow ) {
762
			$relative_url = 'authorize_iframe';
763
		}
764
765
		/**
766
		 * Filters the API URL that Jetpack uses for server communication.
767
		 *
768
		 * @since 8.0.0
769
		 *
770
		 * @param String $url the generated URL.
771
		 * @param String $relative_url the relative URL that was passed as an argument.
772
		 * @param String $api_base the API base string that is being used.
773
		 * @param String $api_version the API version string that is being used.
774
		 */
775
		return apply_filters(
776
			'jetpack_api_url',
777
			rtrim( $api_base . $relative_url, '/\\' ) . $api_version,
778
			$relative_url,
779
			$api_base,
780
			$api_version
781
		);
782
	}
783
784
	/**
785
	 * Returns the Jetpack XMLRPC WordPress.com API endpoint URL.
786
	 *
787
	 * @return String XMLRPC API URL.
788
	 */
789
	public function xmlrpc_api_url() {
790
		$base = preg_replace(
791
			'#(https?://[^?/]+)(/?.*)?$#',
792
			'\\1',
793
			Constants::get_constant( 'JETPACK__API_BASE' )
794
		);
795
		return untrailingslashit( $base ) . '/xmlrpc.php';
796
	}
797
798
	/**
799
	 * Attempts Jetpack registration which sets up the site for connection. Should
800
	 * remain public because the call to action comes from the current site, not from
801
	 * WordPress.com.
802
	 *
803
	 * @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'.
804
	 * @return Integer zero on success, or a bitmask on failure.
805
	 */
806
	public function register( $api_endpoint = 'register' ) {
807
		add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) );
808
		$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 );
809
810
		if (
811
			empty( $secrets['secret_1'] ) ||
812
			empty( $secrets['secret_2'] ) ||
813
			empty( $secrets['exp'] )
814
		) {
815
			return new \WP_Error( 'missing_secrets' );
816
		}
817
818
		// Better to try (and fail) to set a higher timeout than this system
819
		// supports than to have register fail for more users than it should.
820
		$timeout = $this->set_min_time_limit( 60 ) / 2;
821
822
		$gmt_offset = get_option( 'gmt_offset' );
823
		if ( ! $gmt_offset ) {
824
			$gmt_offset = 0;
825
		}
826
827
		$stats_options = get_option( 'stats_options' );
828
		$stats_id      = isset( $stats_options['blog_id'] )
829
			? $stats_options['blog_id']
830
			: null;
831
832
		/**
833
		 * Filters the request body for additional property addition.
834
		 *
835
		 * @since 7.7.0
836
		 *
837
		 * @param array $post_data request data.
838
		 * @param Array $token_data token data.
839
		 */
840
		$body = apply_filters(
841
			'jetpack_register_request_body',
842
			array(
843
				'siteurl'         => site_url(),
844
				'home'            => home_url(),
845
				'gmt_offset'      => $gmt_offset,
846
				'timezone_string' => (string) get_option( 'timezone_string' ),
847
				'site_name'       => (string) get_option( 'blogname' ),
848
				'secret_1'        => $secrets['secret_1'],
849
				'secret_2'        => $secrets['secret_2'],
850
				'site_lang'       => get_locale(),
851
				'timeout'         => $timeout,
852
				'stats_id'        => $stats_id,
853
				'state'           => get_current_user_id(),
854
				'site_created'    => $this->get_assumed_site_creation_date(),
855
				'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ),
856
			)
857
		);
858
859
		$args = array(
860
			'method'  => 'POST',
861
			'body'    => $body,
862
			'headers' => array(
863
				'Accept' => 'application/json',
864
			),
865
			'timeout' => $timeout,
866
		);
867
868
		$args['body'] = $this->apply_activation_source_to_args( $args['body'] );
869
870
		// TODO: fix URLs for bad hosts.
871
		$response = Client::_wp_remote_request(
872
			$this->api_url( $api_endpoint ),
873
			$args,
874
			true
875
		);
876
877
		// Make sure the response is valid and does not contain any Jetpack errors.
878
		$registration_details = $this->validate_remote_register_response( $response );
879
880
		if ( is_wp_error( $registration_details ) ) {
881
			return $registration_details;
882
		} elseif ( ! $registration_details ) {
883
			return new \WP_Error(
884
				'unknown_error',
885
				'Unknown error registering your Jetpack site.',
886
				wp_remote_retrieve_response_code( $response )
887
			);
888
		}
889
890
		if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) {
891
			return new \WP_Error(
892
				'jetpack_secret',
893
				'Unable to validate registration of your Jetpack site.',
894
				wp_remote_retrieve_response_code( $response )
895
			);
896
		}
897
898
		if ( isset( $registration_details->jetpack_public ) ) {
899
			$jetpack_public = (int) $registration_details->jetpack_public;
900
		} else {
901
			$jetpack_public = false;
902
		}
903
904
		\Jetpack_Options::update_options(
905
			array(
906
				'id'         => (int) $registration_details->jetpack_id,
907
				'blog_token' => (string) $registration_details->jetpack_secret,
908
				'public'     => $jetpack_public,
909
			)
910
		);
911
912
		/**
913
		 * Fires when a site is registered on WordPress.com.
914
		 *
915
		 * @since 3.7.0
916
		 *
917
		 * @param int $json->jetpack_id Jetpack Blog ID.
918
		 * @param string $json->jetpack_secret Jetpack Blog Token.
919
		 * @param int|bool $jetpack_public Is the site public.
920
		 */
921
		do_action(
922
			'jetpack_site_registered',
923
			$registration_details->jetpack_id,
924
			$registration_details->jetpack_secret,
925
			$jetpack_public
926
		);
927
928
		if ( isset( $registration_details->token ) ) {
929
			/**
930
			 * Fires when a user token is sent along with the registration data.
931
			 *
932
			 * @since 7.6.0
933
			 *
934
			 * @param object $token the administrator token for the newly registered site.
935
			 */
936
			do_action( 'jetpack_site_registered_user_token', $registration_details->token );
937
		}
938
939
		return true;
940
	}
941
942
	/**
943
	 * Takes the response from the Jetpack register new site endpoint and
944
	 * verifies it worked properly.
945
	 *
946
	 * @since 2.6
947
	 *
948
	 * @param Mixed $response the response object, or the error object.
949
	 * @return string|WP_Error A JSON object on success or Jetpack_Error on failures
950
	 **/
951
	protected function validate_remote_register_response( $response ) {
952
		if ( is_wp_error( $response ) ) {
953
			return new \WP_Error(
954
				'register_http_request_failed',
955
				$response->get_error_message()
956
			);
957
		}
958
959
		$code   = wp_remote_retrieve_response_code( $response );
960
		$entity = wp_remote_retrieve_body( $response );
961
962
		if ( $entity ) {
963
			$registration_response = json_decode( $entity );
964
		} else {
965
			$registration_response = false;
966
		}
967
968
		$code_type = intval( $code / 100 );
969
		if ( 5 === $code_type ) {
970
			return new \WP_Error( 'wpcom_5??', $code );
971
		} elseif ( 408 === $code ) {
972
			return new \WP_Error( 'wpcom_408', $code );
973
		} elseif ( ! empty( $registration_response->error ) ) {
974
			if (
975
				'xml_rpc-32700' === $registration_response->error
976
				&& ! function_exists( 'xml_parser_create' )
977
			) {
978
				$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' );
979
			} else {
980
				$error_description = isset( $registration_response->error_description )
981
					? (string) $registration_response->error_description
982
					: '';
983
			}
984
985
			return new \WP_Error(
986
				(string) $registration_response->error,
987
				$error_description,
988
				$code
989
			);
990
		} elseif ( 200 !== $code ) {
991
			return new \WP_Error( 'wpcom_bad_response', $code );
992
		}
993
994
		// Jetpack ID error block.
995
		if ( empty( $registration_response->jetpack_id ) ) {
996
			return new \WP_Error(
997
				'jetpack_id',
998
				/* translators: %s is an error message string */
999
				sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1000
				$entity
1001
			);
1002
		} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) {
1003
			return new \WP_Error(
1004
				'jetpack_id',
1005
				/* translators: %s is an error message string */
1006
				sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1007
				$entity
1008
			);
1009 View Code Duplication
		} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) {
1010
			return new \WP_Error(
1011
				'jetpack_id',
1012
				/* translators: %s is an error message string */
1013
				sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
1014
				$entity
1015
			);
1016
		}
1017
1018
		return $registration_response;
1019
	}
1020
1021
	/**
1022
	 * Adds a used nonce to a list of known nonces.
1023
	 *
1024
	 * @param int    $timestamp the current request timestamp.
1025
	 * @param string $nonce the nonce value.
1026
	 * @return bool whether the nonce is unique or not.
1027
	 */
1028
	public function add_nonce( $timestamp, $nonce ) {
1029
		global $wpdb;
1030
		static $nonces_used_this_request = array();
1031
1032
		if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) {
1033
			return $nonces_used_this_request[ "$timestamp:$nonce" ];
1034
		}
1035
1036
		// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce.
1037
		$timestamp = (int) $timestamp;
1038
		$nonce     = esc_sql( $nonce );
1039
1040
		// Raw query so we can avoid races: add_option will also update.
1041
		$show_errors = $wpdb->show_errors( false );
1042
1043
		$old_nonce = $wpdb->get_row(
1044
			$wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" )
1045
		);
1046
1047
		if ( is_null( $old_nonce ) ) {
1048
			$return = $wpdb->query(
1049
				$wpdb->prepare(
1050
					"INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)",
1051
					"jetpack_nonce_{$timestamp}_{$nonce}",
1052
					time(),
1053
					'no'
1054
				)
1055
			);
1056
		} else {
1057
			$return = false;
1058
		}
1059
1060
		$wpdb->show_errors( $show_errors );
1061
1062
		$nonces_used_this_request[ "$timestamp:$nonce" ] = $return;
1063
1064
		return $return;
1065
	}
1066
1067
	/**
1068
	 * Cleans nonces that were saved when calling ::add_nonce.
1069
	 *
1070
	 * @todo Properly prepare the query before executing it.
1071
	 *
1072
	 * @param bool $all whether to clean even non-expired nonces.
1073
	 */
1074
	public function clean_nonces( $all = false ) {
1075
		global $wpdb;
1076
1077
		$sql      = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s";
1078
		$sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' );
1079
1080
		if ( true !== $all ) {
1081
			$sql       .= ' AND CAST( `option_value` AS UNSIGNED ) < %d';
1082
			$sql_args[] = time() - 3600;
1083
		}
1084
1085
		$sql .= ' ORDER BY `option_id` LIMIT 100';
1086
1087
		$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1088
1089
		for ( $i = 0; $i < 1000; $i++ ) {
1090
			if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1091
				break;
1092
			}
1093
		}
1094
	}
1095
1096
	/**
1097
	 * Builds the timeout limit for queries talking with the wpcom servers.
1098
	 *
1099
	 * Based on local php max_execution_time in php.ini
1100
	 *
1101
	 * @since 5.4
1102
	 * @return int
1103
	 **/
1104
	public function get_max_execution_time() {
1105
		$timeout = (int) ini_get( 'max_execution_time' );
1106
1107
		// Ensure exec time set in php.ini.
1108
		if ( ! $timeout ) {
1109
			$timeout = 30;
1110
		}
1111
		return $timeout;
1112
	}
1113
1114
	/**
1115
	 * Sets a minimum request timeout, and returns the current timeout
1116
	 *
1117
	 * @since 5.4
1118
	 * @param Integer $min_timeout the minimum timeout value.
1119
	 **/
1120 View Code Duplication
	public function set_min_time_limit( $min_timeout ) {
1121
		$timeout = $this->get_max_execution_time();
1122
		if ( $timeout < $min_timeout ) {
1123
			$timeout = $min_timeout;
1124
			set_time_limit( $timeout );
1125
		}
1126
		return $timeout;
1127
	}
1128
1129
	/**
1130
	 * Get our assumed site creation date.
1131
	 * Calculated based on the earlier date of either:
1132
	 * - Earliest admin user registration date.
1133
	 * - Earliest date of post of any post type.
1134
	 *
1135
	 * @since 7.2.0
1136
	 *
1137
	 * @return string Assumed site creation date and time.
1138
	 */
1139
	public function get_assumed_site_creation_date() {
1140
		$cached_date = get_transient( 'jetpack_assumed_site_creation_date' );
1141
		if ( ! empty( $cached_date ) ) {
1142
			return $cached_date;
1143
		}
1144
1145
		$earliest_registered_users  = get_users(
1146
			array(
1147
				'role'    => 'administrator',
1148
				'orderby' => 'user_registered',
1149
				'order'   => 'ASC',
1150
				'fields'  => array( 'user_registered' ),
1151
				'number'  => 1,
1152
			)
1153
		);
1154
		$earliest_registration_date = $earliest_registered_users[0]->user_registered;
1155
1156
		$earliest_posts = get_posts(
1157
			array(
1158
				'posts_per_page' => 1,
1159
				'post_type'      => 'any',
1160
				'post_status'    => 'any',
1161
				'orderby'        => 'date',
1162
				'order'          => 'ASC',
1163
			)
1164
		);
1165
1166
		// If there are no posts at all, we'll count only on user registration date.
1167
		if ( $earliest_posts ) {
1168
			$earliest_post_date = $earliest_posts[0]->post_date;
1169
		} else {
1170
			$earliest_post_date = PHP_INT_MAX;
1171
		}
1172
1173
		$assumed_date = min( $earliest_registration_date, $earliest_post_date );
1174
		set_transient( 'jetpack_assumed_site_creation_date', $assumed_date );
1175
1176
		return $assumed_date;
1177
	}
1178
1179
	/**
1180
	 * Adds the activation source string as a parameter to passed arguments.
1181
	 *
1182
	 * @todo Refactor to use rawurlencode() instead of urlencode().
1183
	 *
1184
	 * @param array $args arguments that need to have the source added.
1185
	 * @return array $amended arguments.
1186
	 */
1187 View Code Duplication
	public static function apply_activation_source_to_args( $args ) {
1188
		list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' );
1189
1190
		if ( $activation_source_name ) {
1191
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1192
			$args['_as'] = urlencode( $activation_source_name );
1193
		}
1194
1195
		if ( $activation_source_keyword ) {
1196
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode
1197
			$args['_ak'] = urlencode( $activation_source_keyword );
1198
		}
1199
1200
		return $args;
1201
	}
1202
1203
	/**
1204
	 * Returns the callable that would be used to generate secrets.
1205
	 *
1206
	 * @return Callable a function that returns a secure string to be used as a secret.
1207
	 */
1208
	protected function get_secret_callable() {
1209
		if ( ! isset( $this->secret_callable ) ) {
1210
			/**
1211
			 * Allows modification of the callable that is used to generate connection secrets.
1212
			 *
1213
			 * @param Callable a function or method that returns a secret string.
1214
			 */
1215
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', array( $this, 'secret_callable_method' ) );
1216
		}
1217
1218
		return $this->secret_callable;
1219
	}
1220
1221
	/**
1222
	 * Runs the wp_generate_password function with the required parameters. This is the
1223
	 * default implementation of the secret callable, can be overridden using the
1224
	 * jetpack_connection_secret_generator filter.
1225
	 *
1226
	 * @return String $secret value.
1227
	 */
1228
	private function secret_callable_method() {
1229
		return wp_generate_password( 32, false );
1230
	}
1231
1232
	/**
1233
	 * Generates two secret tokens and the end of life timestamp for them.
1234
	 *
1235
	 * @param String  $action  The action name.
1236
	 * @param Integer $user_id The user identifier.
1237
	 * @param Integer $exp     Expiration time in seconds.
1238
	 */
1239
	public function generate_secrets( $action, $user_id = false, $exp = 600 ) {
1240
		if ( false === $user_id ) {
1241
			$user_id = get_current_user_id();
1242
		}
1243
1244
		$callable = $this->get_secret_callable();
1245
1246
		$secrets = \Jetpack_Options::get_raw_option(
1247
			self::SECRETS_OPTION_NAME,
1248
			array()
1249
		);
1250
1251
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1252
1253
		if (
1254
			isset( $secrets[ $secret_name ] ) &&
1255
			$secrets[ $secret_name ]['exp'] > time()
1256
		) {
1257
			return $secrets[ $secret_name ];
1258
		}
1259
1260
		$secret_value = array(
1261
			'secret_1' => call_user_func( $callable ),
1262
			'secret_2' => call_user_func( $callable ),
1263
			'exp'      => time() + $exp,
1264
		);
1265
1266
		$secrets[ $secret_name ] = $secret_value;
1267
1268
		\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1269
		return $secrets[ $secret_name ];
1270
	}
1271
1272
	/**
1273
	 * Returns two secret tokens and the end of life timestamp for them.
1274
	 *
1275
	 * @param String  $action  The action name.
1276
	 * @param Integer $user_id The user identifier.
1277
	 * @return string|array an array of secrets or an error string.
1278
	 */
1279
	public function get_secrets( $action, $user_id ) {
1280
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1281
		$secrets     = \Jetpack_Options::get_raw_option(
1282
			self::SECRETS_OPTION_NAME,
1283
			array()
1284
		);
1285
1286
		if ( ! isset( $secrets[ $secret_name ] ) ) {
1287
			return self::SECRETS_MISSING;
1288
		}
1289
1290
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
1291
			$this->delete_secrets( $action, $user_id );
1292
			return self::SECRETS_EXPIRED;
1293
		}
1294
1295
		return $secrets[ $secret_name ];
1296
	}
1297
1298
	/**
1299
	 * Deletes secret tokens in case they, for example, have expired.
1300
	 *
1301
	 * @param String  $action  The action name.
1302
	 * @param Integer $user_id The user identifier.
1303
	 */
1304
	public function delete_secrets( $action, $user_id ) {
1305
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1306
		$secrets     = \Jetpack_Options::get_raw_option(
1307
			self::SECRETS_OPTION_NAME,
1308
			array()
1309
		);
1310
		if ( isset( $secrets[ $secret_name ] ) ) {
1311
			unset( $secrets[ $secret_name ] );
1312
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1313
		}
1314
	}
1315
1316
	/**
1317
	 * Deletes all connection tokens and transients from the local Jetpack site.
1318
	 */
1319
	public function delete_all_connection_tokens() {
1320
		\Jetpack_Options::delete_option(
1321
			array(
1322
				'blog_token',
1323
				'user_token',
1324
				'user_tokens',
1325
				'master_user',
1326
				'time_diff',
1327
				'fallback_no_verify_ssl_certs',
1328
			)
1329
		);
1330
1331
		\Jetpack_Options::delete_raw_option( 'jetpack_secrets' );
1332
1333
		// Delete cached connected user data.
1334
		$transient_key = 'jetpack_connected_user_data_' . get_current_user_id();
1335
		delete_transient( $transient_key );
1336
	}
1337
1338
	/**
1339
	 * Tells WordPress.com to disconnect the site and clear all tokens from cached site.
1340
	 */
1341
	public function disconnect_site_wpcom() {
1342
		$xml = new \Jetpack_IXR_Client();
1343
		$xml->query( 'jetpack.deregister', get_current_user_id() );
1344
	}
1345
1346
	/**
1347
	 * Responds to a WordPress.com call to register the current site.
1348
	 * Should be changed to protected.
1349
	 *
1350
	 * @param array $registration_data Array of [ secret_1, user_id ].
1351
	 */
1352
	public function handle_registration( array $registration_data ) {
1353
		list( $registration_secret_1, $registration_user_id ) = $registration_data;
1354
		if ( empty( $registration_user_id ) ) {
1355
			return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 );
1356
		}
1357
1358
		return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id );
1359
	}
1360
1361
	/**
1362
	 * Verify a Previously Generated Secret.
1363
	 *
1364
	 * @param string $action   The type of secret to verify.
1365
	 * @param string $secret_1 The secret string to compare to what is stored.
1366
	 * @param int    $user_id  The user ID of the owner of the secret.
1367
	 * @return \WP_Error|string WP_Error on failure, secret_2 on success.
1368
	 */
1369
	public function verify_secrets( $action, $secret_1, $user_id ) {
1370
		$allowed_actions = array( 'register', 'authorize', 'publicize' );
1371
		if ( ! in_array( $action, $allowed_actions, true ) ) {
1372
			return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
1373
		}
1374
1375
		$user = get_user_by( 'id', $user_id );
1376
1377
		/**
1378
		 * We've begun verifying the previously generated secret.
1379
		 *
1380
		 * @since 7.5.0
1381
		 *
1382
		 * @param string   $action The type of secret to verify.
1383
		 * @param \WP_User $user The user object.
1384
		 */
1385
		do_action( 'jetpack_verify_secrets_begin', $action, $user );
1386
1387
		$return_error = function( \WP_Error $error ) use ( $action, $user ) {
1388
			/**
1389
			 * Verifying of the previously generated secret has failed.
1390
			 *
1391
			 * @since 7.5.0
1392
			 *
1393
			 * @param string    $action  The type of secret to verify.
1394
			 * @param \WP_User  $user The user object.
1395
			 * @param \WP_Error $error The error object.
1396
			 */
1397
			do_action( 'jetpack_verify_secrets_fail', $action, $user, $error );
1398
1399
			return $error;
1400
		};
1401
1402
		$stored_secrets = $this->get_secrets( $action, $user_id );
1403
		$this->delete_secrets( $action, $user_id );
1404
1405
		$error = null;
1406
		if ( empty( $secret_1 ) ) {
1407
			$error = $return_error(
1408
				new \WP_Error(
1409
					'verify_secret_1_missing',
1410
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1411
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ),
1412
					400
1413
				)
1414
			);
1415
		} elseif ( ! is_string( $secret_1 ) ) {
1416
			$error = $return_error(
1417
				new \WP_Error(
1418
					'verify_secret_1_malformed',
1419
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1420
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ),
1421
					400
1422
				)
1423
			);
1424
		} elseif ( empty( $user_id ) ) {
1425
			// $user_id is passed around during registration as "state".
1426
			$error = $return_error(
1427
				new \WP_Error(
1428
					'state_missing',
1429
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1430
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ),
1431
					400
1432
				)
1433
			);
1434
		} elseif ( ! ctype_digit( (string) $user_id ) ) {
1435
			$error = $return_error(
1436
				new \WP_Error(
1437
					'state_malformed',
1438
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1439
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ),
1440
					400
1441
				)
1442
			);
1443
		} elseif ( self::SECRETS_MISSING === $stored_secrets ) {
1444
			$error = $return_error(
1445
				new \WP_Error(
1446
					'verify_secrets_missing',
1447
					__( 'Verification secrets not found', 'jetpack' ),
1448
					400
1449
				)
1450
			);
1451
		} elseif ( self::SECRETS_EXPIRED === $stored_secrets ) {
1452
			$error = $return_error(
1453
				new \WP_Error(
1454
					'verify_secrets_expired',
1455
					__( 'Verification took too long', 'jetpack' ),
1456
					400
1457
				)
1458
			);
1459
		} elseif ( ! $stored_secrets ) {
1460
			$error = $return_error(
1461
				new \WP_Error(
1462
					'verify_secrets_empty',
1463
					__( 'Verification secrets are empty', 'jetpack' ),
1464
					400
1465
				)
1466
			);
1467
		} elseif ( is_wp_error( $stored_secrets ) ) {
1468
			$stored_secrets->add_data( 400 );
1469
			$error = $return_error( $stored_secrets );
1470
		} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
1471
			$error = $return_error(
1472
				new \WP_Error(
1473
					'verify_secrets_incomplete',
1474
					__( 'Verification secrets are incomplete', 'jetpack' ),
1475
					400
1476
				)
1477
			);
1478
		} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
1479
			$error = $return_error(
1480
				new \WP_Error(
1481
					'verify_secrets_mismatch',
1482
					__( 'Secret mismatch', 'jetpack' ),
1483
					400
1484
				)
1485
			);
1486
		}
1487
1488
		// Something went wrong during the checks, returning the error.
1489
		if ( ! empty( $error ) ) {
1490
			return $error;
1491
		}
1492
1493
		/**
1494
		 * We've succeeded at verifying the previously generated secret.
1495
		 *
1496
		 * @since 7.5.0
1497
		 *
1498
		 * @param string   $action The type of secret to verify.
1499
		 * @param \WP_User $user The user object.
1500
		 */
1501
		do_action( 'jetpack_verify_secrets_success', $action, $user );
1502
1503
		return $stored_secrets['secret_2'];
1504
	}
1505
1506
	/**
1507
	 * Responds to a WordPress.com call to authorize the current user.
1508
	 * Should be changed to protected.
1509
	 */
1510
	public function handle_authorization() {
1511
1512
	}
1513
1514
	/**
1515
	 * Obtains the auth token.
1516
	 *
1517
	 * @param array $data The request data.
1518
	 * @return object|\WP_Error Returns the auth token on success.
1519
	 *                          Returns a \WP_Error on failure.
1520
	 */
1521
	public function get_token( $data ) {
1522
		$roles = new Roles();
1523
		$role  = $roles->translate_current_user_to_role();
1524
1525
		if ( ! $role ) {
1526
			return new \WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack' ) );
1527
		}
1528
1529
		$client_secret = $this->get_access_token();
1530
		if ( ! $client_secret ) {
1531
			return new \WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack' ) );
1532
		}
1533
1534
		/**
1535
		 * Filter the URL of the first time the user gets redirected back to your site for connection
1536
		 * data processing.
1537
		 *
1538
		 * @since 8.0.0
1539
		 *
1540
		 * @param string $redirect_url Defaults to the site admin URL.
1541
		 */
1542
		$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
1543
1544
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
1545
1546
		/**
1547
		* Filter the URL to redirect the user back to when the authentication process
1548
		* is complete.
1549
		*
1550
		* @since 8.0.0
1551
		*
1552
		* @param string $redirect_url Defaults to the site URL.
1553
		*/
1554
		$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
1555
1556
		$redirect_uri = ( 'calypso' === $data['auth_type'] )
1557
			? $data['redirect_uri']
1558
			: add_query_arg(
1559
				array(
1560
					'action'   => 'authorize',
1561
					'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
1562
					'redirect' => $redirect ? rawurlencode( $redirect ) : false,
1563
				),
1564
				esc_url( $processing_url )
1565
			);
1566
1567
		/**
1568
		 * Filters the token request data.
1569
		 *
1570
		 * @since 8.0.0
1571
		 *
1572
		 * @param array $request_data request data.
1573
		 */
1574
		$body = apply_filters(
1575
			'jetpack_token_request_body',
1576
			array(
1577
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
1578
				'client_secret' => $client_secret->secret,
1579
				'grant_type'    => 'authorization_code',
1580
				'code'          => $data['code'],
1581
				'redirect_uri'  => $redirect_uri,
1582
			)
1583
		);
1584
1585
		$args = array(
1586
			'method'  => 'POST',
1587
			'body'    => $body,
1588
			'headers' => array(
1589
				'Accept' => 'application/json',
1590
			),
1591
		);
1592
1593
		add_filter( 'http_request_timeout', array( $this, 'increase_timeout' ), PHP_INT_MAX - 1 );
1594
		$response = Client::_wp_remote_request( Utils::fix_url_for_bad_hosts( $this->api_url( 'token' ) ), $args );
1595
		remove_filter( 'http_request_timeout', array( $this, 'increase_timeout' ), PHP_INT_MAX - 1 );
1596
1597
		if ( is_wp_error( $response ) ) {
1598
			return new \WP_Error( 'token_http_request_failed', $response->get_error_message() );
1599
		}
1600
1601
		$code   = wp_remote_retrieve_response_code( $response );
1602
		$entity = wp_remote_retrieve_body( $response );
1603
1604
		if ( $entity ) {
1605
			$json = json_decode( $entity );
1606
		} else {
1607
			$json = false;
1608
		}
1609
1610
		if ( 200 !== $code || ! empty( $json->error ) ) {
1611
			if ( empty( $json->error ) ) {
1612
				return new \WP_Error( 'unknown', '', $code );
1613
			}
1614
1615
			/* translators: Error description string. */
1616
			$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : '';
1617
1618
			return new \WP_Error( (string) $json->error, $error_description, $code );
1619
		}
1620
1621
		if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
1622
			return new \WP_Error( 'access_token', '', $code );
1623
		}
1624
1625
		if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
1626
			return new \WP_Error( 'token_type', '', $code );
1627
		}
1628
1629
		if ( empty( $json->scope ) ) {
1630
			return new \WP_Error( 'scope', 'No Scope', $code );
1631
		}
1632
1633
		// TODO: get rid of the error silencer.
1634
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1635
		@list( $role, $hmac ) = explode( ':', $json->scope );
1636
		if ( empty( $role ) || empty( $hmac ) ) {
1637
			return new \WP_Error( 'scope', 'Malformed Scope', $code );
1638
		}
1639
1640
		if ( $this->sign_role( $role ) !== $json->scope ) {
1641
			return new \WP_Error( 'scope', 'Invalid Scope', $code );
1642
		}
1643
1644
		$cap = $roles->translate_role_to_cap( $role );
1645
		if ( ! $cap ) {
1646
			return new \WP_Error( 'scope', 'No Cap', $code );
1647
		}
1648
1649
		if ( ! current_user_can( $cap ) ) {
1650
			return new \WP_Error( 'scope', 'current_user_cannot', $code );
1651
		}
1652
1653
		/**
1654
		 * Fires after user has successfully received an auth token.
1655
		 *
1656
		 * @since 3.9.0
1657
		 */
1658
		do_action( 'jetpack_user_authorized' );
1659
1660
		return (string) $json->access_token;
1661
	}
1662
1663
	/**
1664
	 * Increases the request timeout value to 30 seconds.
1665
	 *
1666
	 * @return int Returns 30.
1667
	 */
1668
	public function increase_timeout() {
1669
		return 30;
1670
	}
1671
1672
	/**
1673
	 * Builds a URL to the Jetpack connection auth page.
1674
	 *
1675
	 * @param WP_User $user (optional) defaults to the current logged in user.
1676
	 * @param String  $redirect (optional) a redirect URL to use instead of the default.
1677
	 * @return string Connect URL.
1678
	 */
1679
	public function get_authorization_url( $user = null, $redirect = null ) {
1680
1681
		if ( empty( $user ) ) {
1682
			$user = wp_get_current_user();
1683
		}
1684
1685
		$roles       = new Roles();
1686
		$role        = $roles->translate_user_to_role( $user );
1687
		$signed_role = $this->sign_role( $role );
1688
1689
		/**
1690
		 * Filter the URL of the first time the user gets redirected back to your site for connection
1691
		 * data processing.
1692
		 *
1693
		 * @since 8.0.0
1694
		 *
1695
		 * @param string $redirect_url Defaults to the site admin URL.
1696
		 */
1697
		$processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) );
1698
1699
		/**
1700
		 * Filter the URL to redirect the user back to when the authorization process
1701
		 * is complete.
1702
		 *
1703
		 * @since 8.0.0
1704
		 *
1705
		 * @param string $redirect_url Defaults to the site URL.
1706
		 */
1707
		$redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect );
1708
1709
		$secrets = $this->generate_secrets( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS );
1710
1711
		/**
1712
		 * Filter the type of authorization.
1713
		 * 'calypso' completes authorization on wordpress.com/jetpack/connect
1714
		 * while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com.
1715
		 *
1716
		 * @since 4.3.3
1717
		 *
1718
		 * @param string $auth_type Defaults to 'calypso', can also be 'jetpack'.
1719
		 */
1720
		$auth_type = apply_filters( 'jetpack_auth_type', 'calypso' );
1721
1722
		/**
1723
		 * Filters the user connection request data for additional property addition.
1724
		 *
1725
		 * @since 8.0.0
1726
		 *
1727
		 * @param array $request_data request data.
1728
		 */
1729
		$body = apply_filters(
1730
			'jetpack_connect_request_body',
1731
			array(
1732
				'response_type' => 'code',
1733
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
1734
				'redirect_uri'  => add_query_arg(
1735
					array(
1736
						'action'   => 'authorize',
1737
						'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
1738
						'redirect' => rawurlencode( $redirect ),
1739
					),
1740
					esc_url( $processing_url )
1741
				),
1742
				'state'         => $user->ID,
1743
				'scope'         => $signed_role,
1744
				'user_email'    => $user->user_email,
1745
				'user_login'    => $user->user_login,
1746
				'is_active'     => $this->is_active(),
1747
				'jp_version'    => Constants::get_constant( 'JETPACK__VERSION' ),
1748
				'auth_type'     => $auth_type,
1749
				'secret'        => $secrets['secret_1'],
1750
				'blogname'      => get_option( 'blogname' ),
1751
				'site_url'      => site_url(),
1752
				'home_url'      => home_url(),
1753
				'site_icon'     => get_site_icon_url(),
1754
				'site_lang'     => get_locale(),
1755
				'site_created'  => $this->get_assumed_site_creation_date(),
1756
			)
1757
		);
1758
1759
		$body = $this->apply_activation_source_to_args( urlencode_deep( $body ) );
1760
1761
		$api_url = $this->api_url( 'authorize' );
1762
1763
		return add_query_arg( $body, $api_url );
1764
	}
1765
1766
	/**
1767
	 * Authorizes the user by obtaining and storing the user token.
1768
	 *
1769
	 * @param array $data The request data.
1770
	 * @return string|\WP_Error Returns a string on success.
1771
	 *                          Returns a \WP_Error on failure.
1772
	 */
1773
	public function authorize( $data = array() ) {
1774
		/**
1775
		 * Action fired when user authorization starts.
1776
		 *
1777
		 * @since 8.0.0
1778
		 */
1779
		do_action( 'jetpack_authorize_starting' );
1780
1781
		$roles = new Roles();
1782
		$role  = $roles->translate_current_user_to_role();
1783
1784
		if ( ! $role ) {
1785
			return new \WP_Error( 'no_role', 'Invalid request.', 400 );
1786
		}
1787
1788
		$cap = $roles->translate_role_to_cap( $role );
1789
		if ( ! $cap ) {
1790
			return new \WP_Error( 'no_cap', 'Invalid request.', 400 );
1791
		}
1792
1793
		if ( ! empty( $data['error'] ) ) {
1794
			return new \WP_Error( $data['error'], 'Error included in the request.', 400 );
1795
		}
1796
1797
		if ( ! isset( $data['state'] ) ) {
1798
			return new \WP_Error( 'no_state', 'Request must include state.', 400 );
1799
		}
1800
1801
		if ( ! ctype_digit( $data['state'] ) ) {
1802
			return new \WP_Error( $data['error'], 'State must be an integer.', 400 );
1803
		}
1804
1805
		$current_user_id = get_current_user_id();
1806
		if ( $current_user_id !== (int) $data['state'] ) {
1807
			return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 );
1808
		}
1809
1810
		if ( empty( $data['code'] ) ) {
1811
			return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 );
1812
		}
1813
1814
		$token = $this->get_token( $data );
1815
1816 View Code Duplication
		if ( is_wp_error( $token ) ) {
1817
			$code = $token->get_error_code();
1818
			if ( empty( $code ) ) {
1819
				$code = 'invalid_token';
1820
			}
1821
			return new \WP_Error( $code, $token->get_error_message(), 400 );
1822
		}
1823
1824
		if ( ! $token ) {
1825
			return new \WP_Error( 'no_token', 'Error generating token.', 400 );
1826
		}
1827
1828
		$is_master_user = ! $this->is_active();
1829
1830
		Utils::update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_master_user );
1831
1832
		if ( ! $is_master_user ) {
1833
			/**
1834
			 * Action fired when a secondary user has been authorized.
1835
			 *
1836
			 * @since 8.0.0
1837
			 */
1838
			do_action( 'jetpack_authorize_ending_linked' );
1839
			return 'linked';
1840
		}
1841
1842
		/**
1843
		 * Action fired when the master user has been authorized.
1844
		 *
1845
		 * @since 8.0.0
1846
		 *
1847
		 * @param array $data The request data.
1848
		 */
1849
		do_action( 'jetpack_authorize_ending_authorized', $data );
1850
1851
		\Jetpack_Options::delete_raw_option( 'jetpack_last_connect_url_check' );
1852
1853
		// Start nonce cleaner.
1854
		wp_clear_scheduled_hook( 'jetpack_clean_nonces' );
1855
		wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
1856
1857
		return 'authorized';
1858
	}
1859
1860
	/**
1861
	 * Disconnects from the Jetpack servers.
1862
	 * Forgets all connection details and tells the Jetpack servers to do the same.
1863
	 */
1864
	public function disconnect_site() {
1865
1866
	}
1867
1868
	/**
1869
	 * The Base64 Encoding of the SHA1 Hash of the Input.
1870
	 *
1871
	 * @param string $text The string to hash.
1872
	 * @return string
1873
	 */
1874
	public function sha1_base64( $text ) {
1875
		return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1876
	}
1877
1878
	/**
1879
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
1880
	 *
1881
	 * @param string $domain The domain to check.
1882
	 *
1883
	 * @return bool|WP_Error
1884
	 */
1885
	public function is_usable_domain( $domain ) {
1886
1887
		// If it's empty, just fail out.
1888
		if ( ! $domain ) {
1889
			return new \WP_Error(
1890
				'fail_domain_empty',
1891
				/* translators: %1$s is a domain name. */
1892
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
1893
			);
1894
		}
1895
1896
		/**
1897
		 * Skips the usuable domain check when connecting a site.
1898
		 *
1899
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
1900
		 *
1901
		 * @since 4.1.0
1902
		 *
1903
		 * @param bool If the check should be skipped. Default false.
1904
		 */
1905
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
1906
			return true;
1907
		}
1908
1909
		// None of the explicit localhosts.
1910
		$forbidden_domains = array(
1911
			'wordpress.com',
1912
			'localhost',
1913
			'localhost.localdomain',
1914
			'127.0.0.1',
1915
			'local.wordpress.test',         // VVV pattern.
1916
			'local.wordpress-trunk.test',   // VVV pattern.
1917
			'src.wordpress-develop.test',   // VVV pattern.
1918
			'build.wordpress-develop.test', // VVV pattern.
1919
		);
1920 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
1921
			return new \WP_Error(
1922
				'fail_domain_forbidden',
1923
				sprintf(
1924
					/* translators: %1$s is a domain name. */
1925
					__(
1926
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
1927
						'jetpack'
1928
					),
1929
					$domain
1930
				)
1931
			);
1932
		}
1933
1934
		// No .test or .local domains.
1935 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
1936
			return new \WP_Error(
1937
				'fail_domain_tld',
1938
				sprintf(
1939
					/* translators: %1$s is a domain name. */
1940
					__(
1941
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
1942
						'jetpack'
1943
					),
1944
					$domain
1945
				)
1946
			);
1947
		}
1948
1949
		// No WPCOM subdomains.
1950 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
1951
			return new \WP_Error(
1952
				'fail_subdomain_wpcom',
1953
				sprintf(
1954
					/* translators: %1$s is a domain name. */
1955
					__(
1956
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
1957
						'jetpack'
1958
					),
1959
					$domain
1960
				)
1961
			);
1962
		}
1963
1964
		// If PHP was compiled without support for the Filter module (very edge case).
1965
		if ( ! function_exists( 'filter_var' ) ) {
1966
			// Just pass back true for now, and let wpcom sort it out.
1967
			return true;
1968
		}
1969
1970
		return true;
1971
	}
1972
1973
	/**
1974
	 * Gets the requested token.
1975
	 *
1976
	 * Tokens are one of two types:
1977
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
1978
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
1979
	 *    are not associated with a user account. They represent the site's connection with
1980
	 *    the Jetpack servers.
1981
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
1982
	 *
1983
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
1984
	 * token, and $private is a secret that should never be displayed anywhere or sent
1985
	 * over the network; it's used only for signing things.
1986
	 *
1987
	 * Blog Tokens can be "Normal" or "Special".
1988
	 * * Normal: The result of a normal connection flow. They look like
1989
	 *   "{$random_string_1}.{$random_string_2}"
1990
	 *   That is, $token_key and $private are both random strings.
1991
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
1992
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
1993
	 *   constant (rare).
1994
	 * * Special: A connection token for sites that have gone through an alternative
1995
	 *   connection flow. They look like:
1996
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
1997
	 *   That is, $private is a random string and $token_key has a special structure with
1998
	 *   lots of semicolons.
1999
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
2000
	 *   JETPACK_BLOG_TOKEN constant.
2001
	 *
2002
	 * In particular, note that Normal Blog Tokens never start with ";" and that
2003
	 * Special Blog Tokens always do.
2004
	 *
2005
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
2006
	 * order:
2007
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
2008
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
2009
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
2010
	 *
2011
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
2012
	 * @param string|false $token_key If provided, check that the token matches the provided input.
2013
	 * @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.
2014
	 *
2015
	 * @return object|false
2016
	 */
2017
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
2018
		$possible_special_tokens = array();
2019
		$possible_normal_tokens  = array();
2020
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
2021
2022
		if ( $user_id ) {
2023
			if ( ! $user_tokens ) {
2024
				return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' );
2025
			}
2026
			if ( self::JETPACK_MASTER_USER === $user_id ) {
2027
				$user_id = \Jetpack_Options::get_option( 'master_user' );
2028
				if ( ! $user_id ) {
2029
					return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' );
2030
				}
2031
			}
2032
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
2033
				return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) );
2034
			}
2035
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
2036 View Code Duplication
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
2037
				return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) );
2038
			}
2039 View Code Duplication
			if ( $user_token_chunks[2] !== (string) $user_id ) {
2040
				return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch', sprintf( 'Requesting user_id %d does not match token user_id %d', $user_id, $user_token_chunks[2] ) );
2041
			}
2042
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
2043
		} else {
2044
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
2045
			if ( $stored_blog_token ) {
2046
				$possible_normal_tokens[] = $stored_blog_token;
2047
			}
2048
2049
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
2050
2051
			if ( $defined_tokens_string ) {
2052
				$defined_tokens = explode( ',', $defined_tokens_string );
2053
				foreach ( $defined_tokens as $defined_token ) {
2054
					if ( ';' === $defined_token[0] ) {
2055
						$possible_special_tokens[] = $defined_token;
2056
					} else {
2057
						$possible_normal_tokens[] = $defined_token;
2058
					}
2059
				}
2060
			}
2061
		}
2062
2063
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
2064
			$possible_tokens = $possible_normal_tokens;
2065
		} else {
2066
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
2067
		}
2068
2069
		if ( ! $possible_tokens ) {
2070
			return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' );
2071
		}
2072
2073
		$valid_token = false;
2074
2075
		if ( false === $token_key ) {
2076
			// Use first token.
2077
			$valid_token = $possible_tokens[0];
2078
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
2079
			// Use first normal token.
2080
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
2081
		} else {
2082
			// Use the token matching $token_key or false if none.
2083
			// Ensure we check the full key.
2084
			$token_check = rtrim( $token_key, '.' ) . '.';
2085
2086
			foreach ( $possible_tokens as $possible_token ) {
2087
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
2088
					$valid_token = $possible_token;
2089
					break;
2090
				}
2091
			}
2092
		}
2093
2094
		if ( ! $valid_token ) {
2095
			return $suppress_errors ? false : new \WP_Error( 'no_valid_token' );
2096
		}
2097
2098
		return (object) array(
2099
			'secret'           => $valid_token,
2100
			'external_user_id' => (int) $user_id,
2101
		);
2102
	}
2103
2104
	/**
2105
	 * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths
2106
	 * since it is passed by reference to various methods.
2107
	 * Capture it here so we can verify the signature later.
2108
	 *
2109
	 * @param array $methods an array of available XMLRPC methods.
2110
	 * @return array the same array, since this method doesn't add or remove anything.
2111
	 */
2112
	public function xmlrpc_methods( $methods ) {
2113
		$this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA'];
2114
		return $methods;
2115
	}
2116
2117
	/**
2118
	 * Resets the raw post data parameter for testing purposes.
2119
	 */
2120
	public function reset_raw_post_data() {
2121
		$this->raw_post_data = null;
2122
	}
2123
2124
	/**
2125
	 * Registering an additional method.
2126
	 *
2127
	 * @param array $methods an array of available XMLRPC methods.
2128
	 * @return array the amended array in case the method is added.
2129
	 */
2130
	public function public_xmlrpc_methods( $methods ) {
2131
		if ( array_key_exists( 'wp.getOptions', $methods ) ) {
2132
			$methods['wp.getOptions'] = array( $this, 'jetpack_get_options' );
2133
		}
2134
		return $methods;
2135
	}
2136
2137
	/**
2138
	 * Handles a getOptions XMLRPC method call.
2139
	 *
2140
	 * @param array $args method call arguments.
2141
	 * @return an amended XMLRPC server options array.
2142
	 */
2143
	public function jetpack_get_options( $args ) {
2144
		global $wp_xmlrpc_server;
2145
2146
		$wp_xmlrpc_server->escape( $args );
2147
2148
		$username = $args[1];
2149
		$password = $args[2];
2150
2151
		$user = $wp_xmlrpc_server->login( $username, $password );
2152
		if ( ! $user ) {
2153
			return $wp_xmlrpc_server->error;
2154
		}
2155
2156
		$options   = array();
2157
		$user_data = $this->get_connected_user_data();
2158
		if ( is_array( $user_data ) ) {
2159
			$options['jetpack_user_id']         = array(
2160
				'desc'     => __( 'The WP.com user ID of the connected user', 'jetpack' ),
2161
				'readonly' => true,
2162
				'value'    => $user_data['ID'],
2163
			);
2164
			$options['jetpack_user_login']      = array(
2165
				'desc'     => __( 'The WP.com username of the connected user', 'jetpack' ),
2166
				'readonly' => true,
2167
				'value'    => $user_data['login'],
2168
			);
2169
			$options['jetpack_user_email']      = array(
2170
				'desc'     => __( 'The WP.com user email of the connected user', 'jetpack' ),
2171
				'readonly' => true,
2172
				'value'    => $user_data['email'],
2173
			);
2174
			$options['jetpack_user_site_count'] = array(
2175
				'desc'     => __( 'The number of sites of the connected WP.com user', 'jetpack' ),
2176
				'readonly' => true,
2177
				'value'    => $user_data['site_count'],
2178
			);
2179
		}
2180
		$wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options );
2181
		$args                           = stripslashes_deep( $args );
2182
		return $wp_xmlrpc_server->wp_getOptions( $args );
2183
	}
2184
2185
	/**
2186
	 * Adds Jetpack-specific options to the output of the XMLRPC options method.
2187
	 *
2188
	 * @param array $options standard Core options.
2189
	 * @return array amended options.
2190
	 */
2191
	public function xmlrpc_options( $options ) {
2192
		$jetpack_client_id = false;
2193
		if ( $this->is_active() ) {
2194
			$jetpack_client_id = \Jetpack_Options::get_option( 'id' );
2195
		}
2196
		$options['jetpack_version'] = array(
2197
			'desc'     => __( 'Jetpack Plugin Version', 'jetpack' ),
2198
			'readonly' => true,
2199
			'value'    => Constants::get_constant( 'JETPACK__VERSION' ),
2200
		);
2201
2202
		$options['jetpack_client_id'] = array(
2203
			'desc'     => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack' ),
2204
			'readonly' => true,
2205
			'value'    => $jetpack_client_id,
2206
		);
2207
		return $options;
2208
	}
2209
2210
	/**
2211
	 * Resets the saved authentication state in between testing requests.
2212
	 */
2213
	public function reset_saved_auth_state() {
2214
		$this->xmlrpc_verification = null;
2215
	}
2216
2217
	/**
2218
	 * Sign a user role with the master access token.
2219
	 * If not specified, will default to the current user.
2220
	 *
2221
	 * @access public
2222
	 *
2223
	 * @param string $role    User role.
2224
	 * @param int    $user_id ID of the user.
2225
	 * @return string Signed user role.
2226
	 */
2227
	public function sign_role( $role, $user_id = null ) {
2228
		if ( empty( $user_id ) ) {
2229
			$user_id = (int) get_current_user_id();
2230
		}
2231
2232
		if ( ! $user_id ) {
2233
			return false;
2234
		}
2235
2236
		$token = $this->get_access_token();
2237
		if ( ! $token || is_wp_error( $token ) ) {
2238
			return false;
2239
		}
2240
2241
		return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
2242
	}
2243
2244
	/**
2245
	 * Set the plugin instance.
2246
	 *
2247
	 * @param Plugin $plugin_instance The plugin instance.
2248
	 *
2249
	 * @return $this
2250
	 */
2251
	public function set_plugin_instance( Plugin $plugin_instance ) {
2252
		$this->plugin = $plugin_instance;
2253
2254
		return $this;
2255
	}
2256
2257
	/**
2258
	 * Retrieve the plugin management object.
2259
	 *
2260
	 * @return Plugin
2261
	 */
2262
	public function get_plugin() {
2263
		return $this->plugin;
2264
	}
2265
2266
	/**
2267
	 * Get all connected plugins information.
2268
	 *
2269
	 * @return array|\WP_Error
2270
	 */
2271
	public function get_connected_plugins() {
2272
		return Plugin_Storage::get_all();
2273
	}
2274
2275
}
2276