Completed
Push — master ( 2d9109...ca302c )
by Marin
08:24
created

packages/connection/src/Manager.php (1 issue)

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 jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
use Automattic\Jetpack\Constants;
11
use Automattic\Jetpack\Tracking;
12
13
/**
14
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
15
 * and Jetpack.
16
 */
17
class Manager implements Manager_Interface {
18
19
	const SECRETS_MISSING        = 'secrets_missing';
20
	const SECRETS_EXPIRED        = 'secrets_expired';
21
	const SECRETS_OPTION_NAME    = 'jetpack_secrets';
22
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
23
	const JETPACK_MASTER_USER    = true;
24
25
	/**
26
	 * The procedure that should be run to generate secrets.
27
	 *
28
	 * @var Callable
29
	 */
30
	protected $secret_callable;
31
32
	/**
33
	 * A copy of the raw POST data for signature verification purposes.
34
	 *
35
	 * @var String
36
	 */
37
	protected $raw_post_data;
38
39
	/**
40
	 * Verification data needs to be stored to properly verify everything.
41
	 *
42
	 * @var Object
43
	 */
44
	private $xmlrpc_verification = null;
45
46
	/**
47
	 * Initializes required listeners. This is done separately from the constructors
48
	 * because some objects sometimes need to instantiate separate objects of this class.
49
	 *
50
	 * @todo Implement a proper nonce verification.
51
	 */
52
	public function init() {
53
54
		$is_jetpack_xmlrpc_request = $this->setup_xmlrpc_handlers(
55
			$_GET,
56
			$this->is_active(),
57
			$this->verify_xml_rpc_signature()
58
		);
59
60
		// All the XMLRPC functionality has been moved into setup_xmlrpc_handlers.
61
		if (
62
			! $is_jetpack_xmlrpc_request
63
			&& is_admin()
64
			&& isset( $_POST['action'] ) // phpcs:ignore WordPress.Security.NonceVerification
65
			&& (
66
				'jetpack_upload_file' === $_POST['action']  // phpcs:ignore WordPress.Security.NonceVerification
67
				|| 'jetpack_update_file' === $_POST['action']  // phpcs:ignore WordPress.Security.NonceVerification
68
			)
69
		) {
70
			$this->require_jetpack_authentication();
71
			$this->add_remote_request_handlers();
72
			return;
73
		}
74
75
		if ( $this->is_active() ) {
76
			add_filter( 'xmlrpc_methods', array( $this, 'public_xmlrpc_methods' ) );
77
		} else {
78
			add_action( 'rest_api_init', array( $this, 'initialize_rest_api_registration_connector' ) );
79
		}
80
	}
81
82
	/**
83
	 * Sets up the XMLRPC request handlers.
84
	 *
85
	 * @param Array                  $request_params incoming request parameters.
86
	 * @param Boolean                $is_active whether the connection is currently active.
87
	 * @param Boolean                $is_signed whether the signature check has been successful.
88
	 * @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one.
89
	 */
90
	public function setup_xmlrpc_handlers(
91
		$request_params,
92
		$is_active,
93
		$is_signed,
94
		\Jetpack_XMLRPC_Server $xmlrpc_server = null
95
	) {
96
		if (
97
			! isset( $request_params['for'] )
98
			|| 'jetpack' !== $request_params['for']
99
		) {
100
			return false;
101
		}
102
103
		// Alternate XML-RPC, via ?for=jetpack&jetpack=comms.
104
		if (
105
			isset( $request_params['jetpack'] )
106
			&& 'comms' === $request_params['jetpack']
107
		) {
108
			if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) {
109
				// Use the real constant here for WordPress' sake.
110
				define( 'XMLRPC_REQUEST', true );
111
			}
112
113
			add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) );
114
115
			add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 );
116
		}
117
118
		if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) {
119
			return false;
120
		}
121
		// Display errors can cause the XML to be not well formed.
122
		@ini_set( 'display_errors', false ); // phpcs:ignore
123
124
		if ( $xmlrpc_server ) {
125
			$this->xmlrpc_server = $xmlrpc_server;
126
		} else {
127
			$this->xmlrpc_server = new \Jetpack_XMLRPC_Server();
128
		}
129
130
		$this->require_jetpack_authentication();
131
132
		if ( $is_active ) {
133
			// Hack to preserve $HTTP_RAW_POST_DATA.
134
			add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
135
136
			if ( $is_signed ) {
137
				// The actual API methods.
138
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) );
139
			} else {
140
				// The jetpack.authorize method should be available for unauthenticated users on a site with an
141
				// active Jetpack connection, so that additional users can link their account.
142
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) );
143
			}
144
		} else {
145
			// The bootstrap API methods.
146
			add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) );
147
148
			if ( $is_signed ) {
149
				// The jetpack Provision method is available for blog-token-signed requests.
150
				add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) );
151
			} else {
152
				new XMLRPC_Connector( $this );
153
			}
154
		}
155
156
		add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ) );
157
158
		add_action( 'jetpack_clean_nonces', array( $this, 'clean_nonces' ) );
159
		if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) {
160
			wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' );
161
		}
162
163
		// Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on.
164
		add_filter( 'pre_option_enable_xmlrpc', '__return_true' );
165
166
		return true;
167
	}
168
169
	/**
170
	 * Initializes the REST API connector on the init hook.
171
	 */
172
	public function initialize_rest_api_registration_connector() {
173
		new REST_Connector( $this );
174
	}
175
176
	/**
177
	 * Since a lot of hosts use a hammer approach to "protecting" WordPress sites,
178
	 * and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive
179
	 * security/firewall policies, we provide our own alternate XML RPC API endpoint
180
	 * which is accessible via a different URI. Most of the below is copied directly
181
	 * from /xmlrpc.php so that we're replicating it as closely as possible.
182
	 *
183
	 * @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things.
184
	 */
185
	public function alternate_xmlrpc() {
186
		// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
187
		// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
188
		global $HTTP_RAW_POST_DATA;
189
190
		// Some browser-embedded clients send cookies. We don't want them.
191
		$_COOKIE = array();
192
193
		// A fix for mozBlog and other cases where '<?xml' isn't on the very first line.
194
		if ( isset( $HTTP_RAW_POST_DATA ) ) {
195
			$HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA );
196
		}
197
198
		// phpcs:enable
199
200
		include_once ABSPATH . 'wp-admin/includes/admin.php';
201
		include_once ABSPATH . WPINC . '/class-IXR.php';
202
		include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php';
203
204
		/**
205
		 * Filters the class used for handling XML-RPC requests.
206
		 *
207
		 * @since 3.1.0
208
		 *
209
		 * @param string $class The name of the XML-RPC server class.
210
		 */
211
		$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' );
212
		$wp_xmlrpc_server       = new $wp_xmlrpc_server_class();
213
214
		// Fire off the request.
215
		nocache_headers();
216
		$wp_xmlrpc_server->serve_request();
217
218
		exit;
219
	}
220
221
	/**
222
	 * Removes all XML-RPC methods that are not `jetpack.*`.
223
	 * Only used in our alternate XML-RPC endpoint, where we want to
224
	 * ensure that Core and other plugins' methods are not exposed.
225
	 *
226
	 * @param array $methods a list of registered WordPress XMLRPC methods.
227
	 * @return array filtered $methods
228
	 */
229
	public function remove_non_jetpack_xmlrpc_methods( $methods ) {
230
		$jetpack_methods = array();
231
232
		foreach ( $methods as $method => $callback ) {
233
			if ( 0 === strpos( $method, 'jetpack.' ) ) {
234
				$jetpack_methods[ $method ] = $callback;
235
			}
236
		}
237
238
		return $jetpack_methods;
239
	}
240
241
	/**
242
	 * Removes all other authentication methods not to allow other
243
	 * methods to validate unauthenticated requests.
244
	 */
245
	public function require_jetpack_authentication() {
246
		// Don't let anyone authenticate.
247
		$_COOKIE = array();
248
		remove_all_filters( 'authenticate' );
249
		remove_all_actions( 'wp_login_failed' );
250
251
		if ( $this->is_active() ) {
252
			// Allow Jetpack authentication.
253
			add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 );
254
		}
255
	}
256
257
	/**
258
	 * Authenticates XML-RPC and other requests from the Jetpack Server
259
	 *
260
	 * @param WP_User|Mixed $user user object if authenticated.
261
	 * @param String        $username username.
262
	 * @param String        $password password string.
263
	 * @return WP_User|Mixed authenticated user or error.
264
	 */
265
	public function authenticate_jetpack( $user, $username, $password ) {
266
		if ( is_a( $user, '\\WP_User' ) ) {
267
			return $user;
268
		}
269
270
		$token_details = $this->verify_xml_rpc_signature();
271
272
		if ( ! $token_details ) {
273
			return $user;
274
		}
275
276
		if ( 'user' !== $token_details['type'] ) {
277
			return $user;
278
		}
279
280
		if ( ! $token_details['user_id'] ) {
281
			return $user;
282
		}
283
284
		nocache_headers();
285
286
		return new \WP_User( $token_details['user_id'] );
287
	}
288
289
	/**
290
	 * Verifies the signature of the current request.
291
	 *
292
	 * @return false|array
293
	 */
294
	public function verify_xml_rpc_signature() {
295
		if ( is_null( $this->xmlrpc_verification ) ) {
296
			$this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature();
297
298
			if ( is_wp_error( $this->xmlrpc_verification ) ) {
299
				/**
300
				 * Action for logging XMLRPC signature verification errors. This data is sensitive.
301
				 *
302
				 * Error codes:
303
				 * - malformed_token
304
				 * - malformed_user_id
305
				 * - unknown_token
306
				 * - could_not_sign
307
				 * - invalid_nonce
308
				 * - signature_mismatch
309
				 *
310
				 * @since 7.5.0
311
				 *
312
				 * @param WP_Error $signature_verification_error The verification error
313
				 */
314
				do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification );
315
			}
316
		}
317
318
		return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification;
319
	}
320
321
	/**
322
	 * Verifies the signature of the current request.
323
	 *
324
	 * This function has side effects and should not be used. Instead,
325
	 * use the memoized version `->verify_xml_rpc_signature()`.
326
	 *
327
	 * @internal
328
	 */
329
	private function internal_verify_xml_rpc_signature() {
330
		// It's not for us.
331
		if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) {
332
			return false;
333
		}
334
335
		$signature_details = array(
336
			'token'     => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '',
337
			'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '',
338
			'nonce'     => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '',
339
			'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '',
340
			'method'    => wp_unslash( $_SERVER['REQUEST_METHOD'] ),
341
			'url'       => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later.
342
			'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '',
343
		);
344
345
		@list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) );
346
		if (
347
			empty( $token_key )
348
		||
349
			empty( $version ) || strval( JETPACK__API_VERSION ) !== $version
350
		) {
351
			return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) );
352
		}
353
354
		if ( '0' === $user_id ) {
355
			$token_type = 'blog';
356
			$user_id    = 0;
357
		} else {
358
			$token_type = 'user';
359
			if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) {
360
				return new \WP_Error(
361
					'malformed_user_id',
362
					'Malformed user_id in request',
363
					compact( 'signature_details' )
364
				);
365
			}
366
			$user_id = (int) $user_id;
367
368
			$user = new \WP_User( $user_id );
369
			if ( ! $user || ! $user->exists() ) {
370
				return new \WP_Error(
371
					'unknown_user',
372
					sprintf( 'User %d does not exist', $user_id ),
373
					compact( 'signature_details' )
374
				);
375
			}
376
		}
377
378
		$token = $this->get_access_token( $user_id, $token_key, false );
379
		if ( is_wp_error( $token ) ) {
380
			$token->add_data( compact( 'signature_details' ) );
381
			return $token;
382
		} elseif ( ! $token ) {
383
			return new \WP_Error(
384
				'unknown_token',
385
				sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ),
386
				compact( 'signature_details' )
387
			);
388
		}
389
390
		$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) );
391
		// phpcs:disable WordPress.Security.NonceVerification.Missing
392
		if ( isset( $_POST['_jetpack_is_multipart'] ) ) {
393
			$post_data   = $_POST;
394
			$file_hashes = array();
395
			foreach ( $post_data as $post_data_key => $post_data_value ) {
396
				if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) {
397
					continue;
398
				}
399
				$post_data_key                 = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) );
400
				$file_hashes[ $post_data_key ] = $post_data_value;
401
			}
402
403
			foreach ( $file_hashes as $post_data_key => $post_data_value ) {
404
				unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] );
405
				$post_data[ $post_data_key ] = $post_data_value;
406
			}
407
408
			ksort( $post_data );
409
410
			$body = http_build_query( stripslashes_deep( $post_data ) );
411
		} elseif ( is_null( $this->raw_post_data ) ) {
412
			$body = file_get_contents( 'php://input' );
413
		} else {
414
			$body = null;
415
		}
416
		// phpcs:enable
417
418
		$signature = $jetpack_signature->sign_current_request(
419
			array( 'body' => is_null( $body ) ? $this->raw_post_data : $body )
420
		);
421
422
		$signature_details['url'] = $jetpack_signature->current_request_url;
423
424
		if ( ! $signature ) {
425
			return new \WP_Error(
426
				'could_not_sign',
427
				'Unknown signature error',
428
				compact( 'signature_details' )
429
			);
430
		} elseif ( is_wp_error( $signature ) ) {
431
			return $signature;
432
		}
433
434
		$timestamp = (int) $_GET['timestamp'];
435
		$nonce     = stripslashes( (string) $_GET['nonce'] );
436
437
		// Use up the nonce regardless of whether the signature matches.
438
		if ( ! $this->add_nonce( $timestamp, $nonce ) ) {
439
			return new \WP_Error(
440
				'invalid_nonce',
441
				'Could not add nonce',
442
				compact( 'signature_details' )
443
			);
444
		}
445
446
		// Be careful about what you do with this debugging data.
447
		// If a malicious requester has access to the expected signature,
448
		// bad things might be possible.
449
		$signature_details['expected'] = $signature;
450
451
		if ( ! hash_equals( $signature, $_GET['signature'] ) ) {
452
			return new \WP_Error(
453
				'signature_mismatch',
454
				'Signature mismatch',
455
				compact( 'signature_details' )
456
			);
457
		}
458
459
		/**
460
		 * Action for additional token checking.
461
		 *
462
		 * @since 7.7.0
463
		 *
464
		 * @param Array $post_data request data.
465
		 * @param Array $token_data token data.
466
		 */
467
		return apply_filters(
468
			'jetpack_signature_check_token',
469
			array(
470
				'type'      => $token_type,
471
				'token_key' => $token_key,
472
				'user_id'   => $token->external_user_id,
473
			),
474
			$token,
475
			$this->raw_post_data
476
		);
477
	}
478
479
	/**
480
	 * Returns true if the current site is connected to WordPress.com.
481
	 *
482
	 * @return Boolean is the site connected?
483
	 */
484
	public function is_active() {
485
		return (bool) $this->get_access_token( self::JETPACK_MASTER_USER );
486
	}
487
488
	/**
489
	 * Returns true if the site has both a token and a blog id, which indicates a site has been registered.
490
	 *
491
	 * @access public
492
	 *
493
	 * @return bool
494
	 */
495
	public function is_registered() {
496
		$blog_id   = \Jetpack_Options::get_option( 'id' );
497
		$has_token = $this->is_active();
498
		return $blog_id && $has_token;
499
	}
500
501
	/**
502
	 * Returns true if the user with the specified identifier is connected to
503
	 * WordPress.com.
504
	 *
505
	 * @param Integer|Boolean $user_id the user identifier.
506
	 * @return Boolean is the user connected?
507
	 */
508
	public function is_user_connected( $user_id = false ) {
509
		$user_id = false === $user_id ? get_current_user_id() : absint( $user_id );
510
		if ( ! $user_id ) {
511
			return false;
512
		}
513
514
		return (bool) $this->get_access_token( $user_id );
515
	}
516
517
	/**
518
	 * Get the wpcom user data of the current|specified connected user.
519
	 *
520
	 * @todo Refactor to properly load the XMLRPC client independently.
521
	 *
522
	 * @param Integer $user_id the user identifier.
523
	 * @return Object the user object.
524
	 */
525 View Code Duplication
	public function get_connected_user_data( $user_id = null ) {
526
		if ( ! $user_id ) {
527
			$user_id = get_current_user_id();
528
		}
529
530
		$transient_key    = "jetpack_connected_user_data_$user_id";
531
		$cached_user_data = get_transient( $transient_key );
532
533
		if ( $cached_user_data ) {
534
			return $cached_user_data;
535
		}
536
537
		\Jetpack::load_xml_rpc_client();
538
		$xml = new \Jetpack_IXR_Client(
539
			array(
540
				'user_id' => $user_id,
541
			)
542
		);
543
		$xml->query( 'wpcom.getUser' );
544
		if ( ! $xml->isError() ) {
545
			$user_data = $xml->getResponse();
546
			set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS );
547
			return $user_data;
548
		}
549
550
		return false;
551
	}
552
553
	/**
554
	 * Returns true if the provided user is the Jetpack connection owner.
555
	 * If user ID is not specified, the current user will be used.
556
	 *
557
	 * @param Integer|Boolean $user_id the user identifier. False for current user.
558
	 * @return Boolean True the user the connection owner, false otherwise.
559
	 */
560
	public function is_connection_owner( $user_id = false ) {
561
		if ( ! $user_id ) {
562
			$user_id = get_current_user_id();
563
		}
564
565
		$user_token = $this->get_access_token( JETPACK_MASTER_USER );
566
567
		return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id;
568
	}
569
570
	/**
571
	 * Unlinks the current user from the linked WordPress.com user.
572
	 *
573
	 * @access public
574
	 * @static
575
	 *
576
	 * @todo Refactor to properly load the XMLRPC client independently.
577
	 *
578
	 * @param Integer $user_id the user identifier.
0 ignored issues
show
Should the type for parameter $user_id not be integer|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...
579
	 * @return Boolean Whether the disconnection of the user was successful.
580
	 */
581
	public static function disconnect_user( $user_id = null ) {
582
		$tokens = \Jetpack_Options::get_option( 'user_tokens' );
583
		if ( ! $tokens ) {
584
			return false;
585
		}
586
587
		$user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id );
588
589
		if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) {
590
			return false;
591
		}
592
593
		if ( ! isset( $tokens[ $user_id ] ) ) {
594
			return false;
595
		}
596
597
		\Jetpack::load_xml_rpc_client();
598
		$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) );
599
		$xml->query( 'jetpack.unlink_user', $user_id );
600
601
		unset( $tokens[ $user_id ] );
602
603
		\Jetpack_Options::update_option( 'user_tokens', $tokens );
604
605
		/**
606
		 * Fires after the current user has been unlinked from WordPress.com.
607
		 *
608
		 * @since 4.1.0
609
		 *
610
		 * @param int $user_id The current user's ID.
611
		 */
612
		do_action( 'jetpack_unlinked_user', $user_id );
613
614
		return true;
615
	}
616
617
	/**
618
	 * Returns the requested Jetpack API URL.
619
	 *
620
	 * @param String $relative_url the relative API path.
621
	 * @return String API URL.
622
	 */
623
	public function api_url( $relative_url ) {
624
		$api_base = Constants::get_constant( 'JETPACK__API_BASE' );
625
		$version  = Constants::get_constant( 'JETPACK__API_VERSION' );
626
627
		$api_base = $api_base ? $api_base : 'https://jetpack.wordpress.com/jetpack.';
628
		$version  = $version ? '/' . $version . '/' : '/1/';
629
630
		return rtrim( $api_base . $relative_url, '/\\' ) . $version;
631
	}
632
633
	/**
634
	 * Attempts Jetpack registration which sets up the site for connection. Should
635
	 * remain public because the call to action comes from the current site, not from
636
	 * WordPress.com.
637
	 *
638
	 * @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'.
639
	 * @return Integer zero on success, or a bitmask on failure.
640
	 */
641
	public function register( $api_endpoint = 'register' ) {
642
		add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) );
643
		$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 );
644
645
		if (
646
			empty( $secrets['secret_1'] ) ||
647
			empty( $secrets['secret_2'] ) ||
648
			empty( $secrets['exp'] )
649
		) {
650
			return new \WP_Error( 'missing_secrets' );
651
		}
652
653
		// Better to try (and fail) to set a higher timeout than this system
654
		// supports than to have register fail for more users than it should.
655
		$timeout = $this->set_min_time_limit( 60 ) / 2;
656
657
		$gmt_offset = get_option( 'gmt_offset' );
658
		if ( ! $gmt_offset ) {
659
			$gmt_offset = 0;
660
		}
661
662
		$stats_options = get_option( 'stats_options' );
663
		$stats_id      = isset( $stats_options['blog_id'] )
664
			? $stats_options['blog_id']
665
			: null;
666
667
		/**
668
		 * Filters the request body for additional property addition.
669
		 *
670
		 * @since 7.7.0
671
		 *
672
		 * @param Array $post_data request data.
673
		 * @param Array $token_data token data.
674
		 */
675
		$body = apply_filters(
676
			'jetpack_register_request_body',
677
			array(
678
				'siteurl'         => site_url(),
679
				'home'            => home_url(),
680
				'gmt_offset'      => $gmt_offset,
681
				'timezone_string' => (string) get_option( 'timezone_string' ),
682
				'site_name'       => (string) get_option( 'blogname' ),
683
				'secret_1'        => $secrets['secret_1'],
684
				'secret_2'        => $secrets['secret_2'],
685
				'site_lang'       => get_locale(),
686
				'timeout'         => $timeout,
687
				'stats_id'        => $stats_id,
688
				'state'           => get_current_user_id(),
689
				'site_created'    => $this->get_assumed_site_creation_date(),
690
				'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ),
691
			)
692
		);
693
694
		$args = array(
695
			'method'  => 'POST',
696
			'body'    => $body,
697
			'headers' => array(
698
				'Accept' => 'application/json',
699
			),
700
			'timeout' => $timeout,
701
		);
702
703
		$args['body'] = $this->apply_activation_source_to_args( $args['body'] );
704
705
		// TODO: fix URLs for bad hosts.
706
		$response = Client::_wp_remote_request(
707
			$this->api_url( $api_endpoint ),
708
			$args,
709
			true
710
		);
711
712
		// Make sure the response is valid and does not contain any Jetpack errors.
713
		$registration_details = $this->validate_remote_register_response( $response );
714
715
		if ( is_wp_error( $registration_details ) ) {
716
			return $registration_details;
717
		} elseif ( ! $registration_details ) {
718
			return new \WP_Error(
719
				'unknown_error',
720
				'Unknown error registering your Jetpack site.',
721
				wp_remote_retrieve_response_code( $response )
722
			);
723
		}
724
725
		if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) {
726
			return new \WP_Error(
727
				'jetpack_secret',
728
				'Unable to validate registration of your Jetpack site.',
729
				wp_remote_retrieve_response_code( $response )
730
			);
731
		}
732
733
		if ( isset( $registration_details->jetpack_public ) ) {
734
			$jetpack_public = (int) $registration_details->jetpack_public;
735
		} else {
736
			$jetpack_public = false;
737
		}
738
739
		\Jetpack_Options::update_options(
740
			array(
741
				'id'         => (int) $registration_details->jetpack_id,
742
				'blog_token' => (string) $registration_details->jetpack_secret,
743
				'public'     => $jetpack_public,
744
			)
745
		);
746
747
		/**
748
		 * Fires when a site is registered on WordPress.com.
749
		 *
750
		 * @since 3.7.0
751
		 *
752
		 * @param int $json->jetpack_id Jetpack Blog ID.
753
		 * @param string $json->jetpack_secret Jetpack Blog Token.
754
		 * @param int|bool $jetpack_public Is the site public.
755
		 */
756
		do_action(
757
			'jetpack_site_registered',
758
			$registration_details->jetpack_id,
759
			$registration_details->jetpack_secret,
760
			$jetpack_public
761
		);
762
763
		if ( isset( $registration_details->token ) ) {
764
			/**
765
			 * Fires when a user token is sent along with the registration data.
766
			 *
767
			 * @since 7.6.0
768
			 *
769
			 * @param object $token the administrator token for the newly registered site.
770
			 */
771
			do_action( 'jetpack_site_registered_user_token', $registration_details->token );
772
		}
773
774
		return true;
775
	}
776
777
	/**
778
	 * Takes the response from the Jetpack register new site endpoint and
779
	 * verifies it worked properly.
780
	 *
781
	 * @since 2.6
782
	 *
783
	 * @param Mixed $response the response object, or the error object.
784
	 * @return string|WP_Error A JSON object on success or Jetpack_Error on failures
785
	 **/
786
	protected function validate_remote_register_response( $response ) {
787
		if ( is_wp_error( $response ) ) {
788
			return new \WP_Error(
789
				'register_http_request_failed',
790
				$response->get_error_message()
791
			);
792
		}
793
794
		$code   = wp_remote_retrieve_response_code( $response );
795
		$entity = wp_remote_retrieve_body( $response );
796
797
		if ( $entity ) {
798
			$registration_response = json_decode( $entity );
799
		} else {
800
			$registration_response = false;
801
		}
802
803
		$code_type = intval( $code / 100 );
804
		if ( 5 === $code_type ) {
805
			return new \WP_Error( 'wpcom_5??', $code );
806
		} elseif ( 408 === $code ) {
807
			return new \WP_Error( 'wpcom_408', $code );
808
		} elseif ( ! empty( $registration_response->error ) ) {
809
			if (
810
				'xml_rpc-32700' === $registration_response->error
811
				&& ! function_exists( 'xml_parser_create' )
812
			) {
813
				$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' );
814
			} else {
815
				$error_description = isset( $registration_response->error_description )
816
					? (string) $registration_response->error_description
817
					: '';
818
			}
819
820
			return new \WP_Error(
821
				(string) $registration_response->error,
822
				$error_description,
823
				$code
824
			);
825
		} elseif ( 200 !== $code ) {
826
			return new \WP_Error( 'wpcom_bad_response', $code );
827
		}
828
829
		// Jetpack ID error block.
830
		if ( empty( $registration_response->jetpack_id ) ) {
831
			return new \WP_Error(
832
				'jetpack_id',
833
				/* translators: %s is an error message string */
834
				sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
835
				$entity
836
			);
837
		} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) {
838
			return new \WP_Error(
839
				'jetpack_id',
840
				/* translators: %s is an error message string */
841
				sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
842
				$entity
843
			);
844
		} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) {
845
			return new \WP_Error(
846
				'jetpack_id',
847
				/* translators: %s is an error message string */
848
				sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
849
				$entity
850
			);
851
		}
852
853
		return $registration_response;
854
	}
855
856
	/**
857
	 * Adds a used nonce to a list of known nonces.
858
	 *
859
	 * @param int    $timestamp the current request timestamp.
860
	 * @param string $nonce the nonce value.
861
	 * @return bool whether the nonce is unique or not.
862
	 */
863
	public function add_nonce( $timestamp, $nonce ) {
864
		global $wpdb;
865
		static $nonces_used_this_request = array();
866
867
		if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) {
868
			return $nonces_used_this_request[ "$timestamp:$nonce" ];
869
		}
870
871
		// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce.
872
		$timestamp = (int) $timestamp;
873
		$nonce     = esc_sql( $nonce );
874
875
		// Raw query so we can avoid races: add_option will also update.
876
		$show_errors = $wpdb->show_errors( false );
877
878
		$old_nonce = $wpdb->get_row(
879
			$wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" )
880
		);
881
882
		if ( is_null( $old_nonce ) ) {
883
			$return = $wpdb->query(
884
				$wpdb->prepare(
885
					"INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)",
886
					"jetpack_nonce_{$timestamp}_{$nonce}",
887
					time(),
888
					'no'
889
				)
890
			);
891
		} else {
892
			$return = false;
893
		}
894
895
		$wpdb->show_errors( $show_errors );
896
897
		$nonces_used_this_request[ "$timestamp:$nonce" ] = $return;
898
899
		return $return;
900
	}
901
902
	/**
903
	 * Cleans nonces that were saved when calling ::add_nonce.
904
	 *
905
	 * @todo Properly prepare the query before executing it.
906
	 *
907
	 * @param bool $all whether to clean even non-expired nonces.
908
	 */
909
	public function clean_nonces( $all = false ) {
910
		global $wpdb;
911
912
		$sql      = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s";
913
		$sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' );
914
915
		if ( true !== $all ) {
916
			$sql       .= ' AND CAST( `option_value` AS UNSIGNED ) < %d';
917
			$sql_args[] = time() - 3600;
918
		}
919
920
		$sql .= ' ORDER BY `option_id` LIMIT 100';
921
922
		$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
923
924
		for ( $i = 0; $i < 1000; $i++ ) {
925
			if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
926
				break;
927
			}
928
		}
929
	}
930
931
	/**
932
	 * Builds the timeout limit for queries talking with the wpcom servers.
933
	 *
934
	 * Based on local php max_execution_time in php.ini
935
	 *
936
	 * @since 5.4
937
	 * @return int
938
	 **/
939
	public function get_max_execution_time() {
940
		$timeout = (int) ini_get( 'max_execution_time' );
941
942
		// Ensure exec time set in php.ini.
943
		if ( ! $timeout ) {
944
			$timeout = 30;
945
		}
946
		return $timeout;
947
	}
948
949
	/**
950
	 * Sets a minimum request timeout, and returns the current timeout
951
	 *
952
	 * @since 5.4
953
	 * @param Integer $min_timeout the minimum timeout value.
954
	 **/
955 View Code Duplication
	public function set_min_time_limit( $min_timeout ) {
956
		$timeout = $this->get_max_execution_time();
957
		if ( $timeout < $min_timeout ) {
958
			$timeout = $min_timeout;
959
			set_time_limit( $timeout );
960
		}
961
		return $timeout;
962
	}
963
964
	/**
965
	 * Get our assumed site creation date.
966
	 * Calculated based on the earlier date of either:
967
	 * - Earliest admin user registration date.
968
	 * - Earliest date of post of any post type.
969
	 *
970
	 * @since 7.2.0
971
	 *
972
	 * @return string Assumed site creation date and time.
973
	 */
974 View Code Duplication
	public function get_assumed_site_creation_date() {
975
		$earliest_registered_users  = get_users(
976
			array(
977
				'role'    => 'administrator',
978
				'orderby' => 'user_registered',
979
				'order'   => 'ASC',
980
				'fields'  => array( 'user_registered' ),
981
				'number'  => 1,
982
			)
983
		);
984
		$earliest_registration_date = $earliest_registered_users[0]->user_registered;
985
986
		$earliest_posts = get_posts(
987
			array(
988
				'posts_per_page' => 1,
989
				'post_type'      => 'any',
990
				'post_status'    => 'any',
991
				'orderby'        => 'date',
992
				'order'          => 'ASC',
993
			)
994
		);
995
996
		// If there are no posts at all, we'll count only on user registration date.
997
		if ( $earliest_posts ) {
998
			$earliest_post_date = $earliest_posts[0]->post_date;
999
		} else {
1000
			$earliest_post_date = PHP_INT_MAX;
1001
		}
1002
1003
		return min( $earliest_registration_date, $earliest_post_date );
1004
	}
1005
1006
	/**
1007
	 * Adds the activation source string as a parameter to passed arguments.
1008
	 *
1009
	 * @param Array $args arguments that need to have the source added.
1010
	 * @return Array $amended arguments.
1011
	 */
1012 View Code Duplication
	public static function apply_activation_source_to_args( $args ) {
1013
		list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' );
1014
1015
		if ( $activation_source_name ) {
1016
			$args['_as'] = urlencode( $activation_source_name );
1017
		}
1018
1019
		if ( $activation_source_keyword ) {
1020
			$args['_ak'] = urlencode( $activation_source_keyword );
1021
		}
1022
1023
		return $args;
1024
	}
1025
1026
	/**
1027
	 * Returns the callable that would be used to generate secrets.
1028
	 *
1029
	 * @return Callable a function that returns a secure string to be used as a secret.
1030
	 */
1031
	protected function get_secret_callable() {
1032
		if ( ! isset( $this->secret_callable ) ) {
1033
			/**
1034
			 * Allows modification of the callable that is used to generate connection secrets.
1035
			 *
1036
			 * @param Callable a function or method that returns a secret string.
1037
			 */
1038
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
1039
		}
1040
1041
		return $this->secret_callable;
1042
	}
1043
1044
	/**
1045
	 * Generates two secret tokens and the end of life timestamp for them.
1046
	 *
1047
	 * @param String  $action  The action name.
1048
	 * @param Integer $user_id The user identifier.
1049
	 * @param Integer $exp     Expiration time in seconds.
1050
	 */
1051
	public function generate_secrets( $action, $user_id, $exp ) {
1052
		$callable = $this->get_secret_callable();
1053
1054
		$secrets = \Jetpack_Options::get_raw_option(
1055
			self::SECRETS_OPTION_NAME,
1056
			array()
1057
		);
1058
1059
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1060
1061
		if (
1062
			isset( $secrets[ $secret_name ] ) &&
1063
			$secrets[ $secret_name ]['exp'] > time()
1064
		) {
1065
			return $secrets[ $secret_name ];
1066
		}
1067
1068
		$secret_value = array(
1069
			'secret_1' => call_user_func( $callable ),
1070
			'secret_2' => call_user_func( $callable ),
1071
			'exp'      => time() + $exp,
1072
		);
1073
1074
		$secrets[ $secret_name ] = $secret_value;
1075
1076
		\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1077
		return $secrets[ $secret_name ];
1078
	}
1079
1080
	/**
1081
	 * Returns two secret tokens and the end of life timestamp for them.
1082
	 *
1083
	 * @param String  $action  The action name.
1084
	 * @param Integer $user_id The user identifier.
1085
	 * @return string|array an array of secrets or an error string.
1086
	 */
1087
	public function get_secrets( $action, $user_id ) {
1088
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1089
		$secrets     = \Jetpack_Options::get_raw_option(
1090
			self::SECRETS_OPTION_NAME,
1091
			array()
1092
		);
1093
1094
		if ( ! isset( $secrets[ $secret_name ] ) ) {
1095
			return self::SECRETS_MISSING;
1096
		}
1097
1098
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
1099
			$this->delete_secrets( $action, $user_id );
1100
			return self::SECRETS_EXPIRED;
1101
		}
1102
1103
		return $secrets[ $secret_name ];
1104
	}
1105
1106
	/**
1107
	 * Deletes secret tokens in case they, for example, have expired.
1108
	 *
1109
	 * @param String  $action  The action name.
1110
	 * @param Integer $user_id The user identifier.
1111
	 */
1112
	public function delete_secrets( $action, $user_id ) {
1113
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
1114
		$secrets     = \Jetpack_Options::get_raw_option(
1115
			self::SECRETS_OPTION_NAME,
1116
			array()
1117
		);
1118
		if ( isset( $secrets[ $secret_name ] ) ) {
1119
			unset( $secrets[ $secret_name ] );
1120
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
1121
		}
1122
	}
1123
1124
	/**
1125
	 * Responds to a WordPress.com call to register the current site.
1126
	 * Should be changed to protected.
1127
	 *
1128
	 * @param array $registration_data Array of [ secret_1, user_id ].
1129
	 */
1130
	public function handle_registration( array $registration_data ) {
1131
		list( $registration_secret_1, $registration_user_id ) = $registration_data;
1132
		if ( empty( $registration_user_id ) ) {
1133
			return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 );
1134
		}
1135
1136
		return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id );
1137
	}
1138
1139
	/**
1140
	 * Verify a Previously Generated Secret.
1141
	 *
1142
	 * @param string $action   The type of secret to verify.
1143
	 * @param string $secret_1 The secret string to compare to what is stored.
1144
	 * @param int    $user_id  The user ID of the owner of the secret.
1145
	 */
1146
	protected function verify_secrets( $action, $secret_1, $user_id ) {
1147
		$allowed_actions = array( 'register', 'authorize', 'publicize' );
1148
		if ( ! in_array( $action, $allowed_actions, true ) ) {
1149
			return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
1150
		}
1151
1152
		$user = get_user_by( 'id', $user_id );
1153
1154
		/**
1155
		 * We've begun verifying the previously generated secret.
1156
		 *
1157
		 * @since 7.5.0
1158
		 *
1159
		 * @param string   $action The type of secret to verify.
1160
		 * @param \WP_User $user The user object.
1161
		 */
1162
		do_action( 'jetpack_verify_secrets_begin', $action, $user );
1163
1164
		$return_error = function( \WP_Error $error ) use ( $action, $user ) {
1165
			/**
1166
			 * Verifying of the previously generated secret has failed.
1167
			 *
1168
			 * @since 7.5.0
1169
			 *
1170
			 * @param string    $action  The type of secret to verify.
1171
			 * @param \WP_User  $user The user object.
1172
			 * @param \WP_Error $error The error object.
1173
			 */
1174
			do_action( 'jetpack_verify_secrets_fail', $action, $user, $error );
1175
1176
			return $error;
1177
		};
1178
1179
		$stored_secrets = $this->get_secrets( $action, $user_id );
1180
		$this->delete_secrets( $action, $user_id );
1181
1182
		if ( empty( $secret_1 ) ) {
1183
			return $return_error(
1184
				new \WP_Error(
1185
					'verify_secret_1_missing',
1186
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1187
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ),
1188
					400
1189
				)
1190
			);
1191
		} elseif ( ! is_string( $secret_1 ) ) {
1192
			return $return_error(
1193
				new \WP_Error(
1194
					'verify_secret_1_malformed',
1195
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1196
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ),
1197
					400
1198
				)
1199
			);
1200
		} elseif ( empty( $user_id ) ) {
1201
			// $user_id is passed around during registration as "state".
1202
			return $return_error(
1203
				new \WP_Error(
1204
					'state_missing',
1205
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1206
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ),
1207
					400
1208
				)
1209
			);
1210
		} elseif ( ! ctype_digit( (string) $user_id ) ) {
1211
			return $return_error(
1212
				new \WP_Error(
1213
					'verify_secret_1_malformed',
1214
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
1215
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ),
1216
					400
1217
				)
1218
			);
1219
		}
1220
1221
		if ( ! $stored_secrets ) {
1222
			return $return_error(
1223
				new \WP_Error(
1224
					'verify_secrets_missing',
1225
					__( 'Verification secrets not found', 'jetpack' ),
1226
					400
1227
				)
1228
			);
1229
		} elseif ( is_wp_error( $stored_secrets ) ) {
1230
			$stored_secrets->add_data( 400 );
1231
			return $return_error( $stored_secrets );
1232
		} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
1233
			return $return_error(
1234
				new \WP_Error(
1235
					'verify_secrets_incomplete',
1236
					__( 'Verification secrets are incomplete', 'jetpack' ),
1237
					400
1238
				)
1239
			);
1240
		} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
1241
			return $return_error(
1242
				new \WP_Error(
1243
					'verify_secrets_mismatch',
1244
					__( 'Secret mismatch', 'jetpack' ),
1245
					400
1246
				)
1247
			);
1248
		}
1249
1250
		/**
1251
		 * We've succeeded at verifying the previously generated secret.
1252
		 *
1253
		 * @since 7.5.0
1254
		 *
1255
		 * @param string   $action The type of secret to verify.
1256
		 * @param \WP_User $user The user object.
1257
		 */
1258
		do_action( 'jetpack_verify_secrets_success', $action, $user );
1259
1260
		return $stored_secrets['secret_2'];
1261
	}
1262
1263
	/**
1264
	 * Responds to a WordPress.com call to authorize the current user.
1265
	 * Should be changed to protected.
1266
	 */
1267
	public function handle_authorization() {
1268
1269
	}
1270
1271
	/**
1272
	 * Builds a URL to the Jetpack connection auth page.
1273
	 * This needs rethinking.
1274
	 *
1275
	 * @param bool        $raw If true, URL will not be escaped.
1276
	 * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
1277
	 *                              If string, will be a custom redirect.
1278
	 * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
1279
	 * @param bool        $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
1280
	 *
1281
	 * @return string Connect URL
1282
	 */
1283
	public function build_connect_url( $raw, $redirect, $from, $register ) {
1284
		return array( $raw, $redirect, $from, $register );
1285
	}
1286
1287
	/**
1288
	 * Disconnects from the Jetpack servers.
1289
	 * Forgets all connection details and tells the Jetpack servers to do the same.
1290
	 */
1291
	public function disconnect_site() {
1292
1293
	}
1294
1295
	/**
1296
	 * The Base64 Encoding of the SHA1 Hash of the Input.
1297
	 *
1298
	 * @param string $text The string to hash.
1299
	 * @return string
1300
	 */
1301
	public function sha1_base64( $text ) {
1302
		return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1303
	}
1304
1305
	/**
1306
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
1307
	 *
1308
	 * @param string $domain The domain to check.
1309
	 *
1310
	 * @return bool|WP_Error
1311
	 */
1312
	public function is_usable_domain( $domain ) {
1313
1314
		// If it's empty, just fail out.
1315
		if ( ! $domain ) {
1316
			return new \WP_Error(
1317
				'fail_domain_empty',
1318
				/* translators: %1$s is a domain name. */
1319
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
1320
			);
1321
		}
1322
1323
		/**
1324
		 * Skips the usuable domain check when connecting a site.
1325
		 *
1326
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
1327
		 *
1328
		 * @since 4.1.0
1329
		 *
1330
		 * @param bool If the check should be skipped. Default false.
1331
		 */
1332
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
1333
			return true;
1334
		}
1335
1336
		// None of the explicit localhosts.
1337
		$forbidden_domains = array(
1338
			'wordpress.com',
1339
			'localhost',
1340
			'localhost.localdomain',
1341
			'127.0.0.1',
1342
			'local.wordpress.test',         // VVV pattern.
1343
			'local.wordpress-trunk.test',   // VVV pattern.
1344
			'src.wordpress-develop.test',   // VVV pattern.
1345
			'build.wordpress-develop.test', // VVV pattern.
1346
		);
1347 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
1348
			return new \WP_Error(
1349
				'fail_domain_forbidden',
1350
				sprintf(
1351
					/* translators: %1$s is a domain name. */
1352
					__(
1353
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
1354
						'jetpack'
1355
					),
1356
					$domain
1357
				)
1358
			);
1359
		}
1360
1361
		// No .test or .local domains.
1362 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
1363
			return new \WP_Error(
1364
				'fail_domain_tld',
1365
				sprintf(
1366
					/* translators: %1$s is a domain name. */
1367
					__(
1368
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
1369
						'jetpack'
1370
					),
1371
					$domain
1372
				)
1373
			);
1374
		}
1375
1376
		// No WPCOM subdomains.
1377 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
1378
			return new \WP_Error(
1379
				'fail_subdomain_wpcom',
1380
				sprintf(
1381
					/* translators: %1$s is a domain name. */
1382
					__(
1383
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
1384
						'jetpack'
1385
					),
1386
					$domain
1387
				)
1388
			);
1389
		}
1390
1391
		// If PHP was compiled without support for the Filter module (very edge case).
1392
		if ( ! function_exists( 'filter_var' ) ) {
1393
			// Just pass back true for now, and let wpcom sort it out.
1394
			return true;
1395
		}
1396
1397
		return true;
1398
	}
1399
1400
	/**
1401
	 * Gets the requested token.
1402
	 *
1403
	 * Tokens are one of two types:
1404
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
1405
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
1406
	 *    are not associated with a user account. They represent the site's connection with
1407
	 *    the Jetpack servers.
1408
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
1409
	 *
1410
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
1411
	 * token, and $private is a secret that should never be displayed anywhere or sent
1412
	 * over the network; it's used only for signing things.
1413
	 *
1414
	 * Blog Tokens can be "Normal" or "Special".
1415
	 * * Normal: The result of a normal connection flow. They look like
1416
	 *   "{$random_string_1}.{$random_string_2}"
1417
	 *   That is, $token_key and $private are both random strings.
1418
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
1419
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
1420
	 *   constant (rare).
1421
	 * * Special: A connection token for sites that have gone through an alternative
1422
	 *   connection flow. They look like:
1423
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
1424
	 *   That is, $private is a random string and $token_key has a special structure with
1425
	 *   lots of semicolons.
1426
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
1427
	 *   JETPACK_BLOG_TOKEN constant.
1428
	 *
1429
	 * In particular, note that Normal Blog Tokens never start with ";" and that
1430
	 * Special Blog Tokens always do.
1431
	 *
1432
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
1433
	 * order:
1434
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
1435
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
1436
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
1437
	 *
1438
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
1439
	 * @param string|false $token_key If provided, check that the token matches the provided input.
1440
	 * @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.
1441
	 *
1442
	 * @return object|false
1443
	 */
1444
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
1445
		$possible_special_tokens = array();
1446
		$possible_normal_tokens  = array();
1447
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
1448
1449
		if ( $user_id ) {
1450
			if ( ! $user_tokens ) {
1451
				return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' );
1452
			}
1453
			if ( self::JETPACK_MASTER_USER === $user_id ) {
1454
				$user_id = \Jetpack_Options::get_option( 'master_user' );
1455
				if ( ! $user_id ) {
1456
					return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' );
1457
				}
1458
			}
1459
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
1460
				return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) );
1461
			}
1462
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
1463
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
1464
				return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) );
1465
			}
1466
			if ( $user_token_chunks[2] !== (string) $user_id ) {
1467
				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] ) );
1468
			}
1469
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
1470
		} else {
1471
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
1472
			if ( $stored_blog_token ) {
1473
				$possible_normal_tokens[] = $stored_blog_token;
1474
			}
1475
1476
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
1477
1478
			if ( $defined_tokens_string ) {
1479
				$defined_tokens = explode( ',', $defined_tokens_string );
1480
				foreach ( $defined_tokens as $defined_token ) {
1481
					if ( ';' === $defined_token[0] ) {
1482
						$possible_special_tokens[] = $defined_token;
1483
					} else {
1484
						$possible_normal_tokens[] = $defined_token;
1485
					}
1486
				}
1487
			}
1488
		}
1489
1490
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
1491
			$possible_tokens = $possible_normal_tokens;
1492
		} else {
1493
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
1494
		}
1495
1496
		if ( ! $possible_tokens ) {
1497
			return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' );
1498
		}
1499
1500
		$valid_token = false;
1501
1502
		if ( false === $token_key ) {
1503
			// Use first token.
1504
			$valid_token = $possible_tokens[0];
1505
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
1506
			// Use first normal token.
1507
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
1508
		} else {
1509
			// Use the token matching $token_key or false if none.
1510
			// Ensure we check the full key.
1511
			$token_check = rtrim( $token_key, '.' ) . '.';
1512
1513
			foreach ( $possible_tokens as $possible_token ) {
1514
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
1515
					$valid_token = $possible_token;
1516
					break;
1517
				}
1518
			}
1519
		}
1520
1521
		if ( ! $valid_token ) {
1522
			return $suppress_errors ? false : new \WP_Error( 'no_valid_token' );
1523
		}
1524
1525
		return (object) array(
1526
			'secret'           => $valid_token,
1527
			'external_user_id' => (int) $user_id,
1528
		);
1529
	}
1530
1531
	/**
1532
	 * In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths
1533
	 * since it is passed by reference to various methods.
1534
	 * Capture it here so we can verify the signature later.
1535
	 *
1536
	 * @param Array $methods an array of available XMLRPC methods.
1537
	 * @return Array the same array, since this method doesn't add or remove anything.
1538
	 */
1539
	public function xmlrpc_methods( $methods ) {
1540
		$this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA'];
1541
		return $methods;
1542
	}
1543
1544
	/**
1545
	 * Resets the raw post data parameter for testing purposes.
1546
	 */
1547
	public function reset_raw_post_data() {
1548
		$this->raw_post_data = null;
1549
	}
1550
1551
	/**
1552
	 * Registering an additional method.
1553
	 *
1554
	 * @param Array $methods an array of available XMLRPC methods.
1555
	 * @return Array the amended array in case the method is added.
1556
	 */
1557
	public function public_xmlrpc_methods( $methods ) {
1558
		if ( array_key_exists( 'wp.getOptions', $methods ) ) {
1559
			$methods['wp.getOptions'] = array( $this, 'jetpack_getOptions' );
1560
		}
1561
		return $methods;
1562
	}
1563
1564
	/**
1565
	 * Handles a getOptions XMLRPC method call.
1566
	 *
1567
	 * @todo Audit whether we really need to use strings without textdomains.
1568
	 *
1569
	 * @param Array $args method call arguments.
1570
	 * @return an amended XMLRPC server options array.
1571
	 */
1572
	public function jetpack_getOptions( $args ) {
1573
		global $wp_xmlrpc_server;
1574
1575
		$wp_xmlrpc_server->escape( $args );
1576
1577
		$username = $args[1];
1578
		$password = $args[2];
1579
1580
		$user = $wp_xmlrpc_server->login( $username, $password );
1581
		if ( ! $user ) {
1582
			return $wp_xmlrpc_server->error;
1583
		}
1584
1585
		$options   = array();
1586
		$user_data = $this->get_connected_user_data();
1587
		if ( is_array( $user_data ) ) {
1588
			$options['jetpack_user_id']         = array(
1589
				'desc'     => __( 'The WP.com user ID of the connected user' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1590
				'readonly' => true,
1591
				'value'    => $user_data['ID'],
1592
			);
1593
			$options['jetpack_user_login']      = array(
1594
				'desc'     => __( 'The WP.com username of the connected user' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1595
				'readonly' => true,
1596
				'value'    => $user_data['login'],
1597
			);
1598
			$options['jetpack_user_email']      = array(
1599
				'desc'     => __( 'The WP.com user email of the connected user' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1600
				'readonly' => true,
1601
				'value'    => $user_data['email'],
1602
			);
1603
			$options['jetpack_user_site_count'] = array(
1604
				'desc'     => __( 'The number of sites of the connected WP.com user' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1605
				'readonly' => true,
1606
				'value'    => $user_data['site_count'],
1607
			);
1608
		}
1609
		$wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options );
1610
		$args                           = stripslashes_deep( $args );
1611
		return $wp_xmlrpc_server->wp_getOptions( $args );
1612
	}
1613
1614
	/**
1615
	 * Adds Jetpack-specific options to the output of the XMLRPC options method.
1616
	 *
1617
	 * @todo Audit whether we really need to use strings without textdomains.
1618
	 *
1619
	 * @param Array $options standard Core options.
1620
	 * @return Array amended options.
1621
	 */
1622
	public function xmlrpc_options( $options ) {
1623
		$jetpack_client_id = false;
1624
		if ( $this->is_active() ) {
1625
			$jetpack_client_id = \Jetpack_Options::get_option( 'id' );
1626
		}
1627
		$options['jetpack_version'] = array(
1628
			'desc'     => __( 'Jetpack Plugin Version' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1629
			'readonly' => true,
1630
			'value'    => Constants::get_constant( 'JETPACK__VERSION' ),
1631
		);
1632
1633
		$options['jetpack_client_id'] = array(
1634
			'desc'     => __( 'The Client ID/WP.com Blog ID of this site' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain
1635
			'readonly' => true,
1636
			'value'    => $jetpack_client_id,
1637
		);
1638
		return $options;
1639
	}
1640
1641
	/**
1642
	 * Resets the saved authentication state in between testing requests.
1643
	 */
1644
	public function reset_saved_auth_state() {
1645
		$this->xmlrpc_verification = null;
1646
	}
1647
}
1648