Completed
Push — add/user-authentication ( 783ce9 )
by
unknown
07:02
created

Manager::apply_activation_source_to_args()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13

Duplication

Lines 13
Ratio 100 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 1
dl 13
loc 13
rs 9.8333
c 0
b 0
f 0
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
	 * Initializes all needed hooks and request handlers. Handles API calls, upload
34
	 * requests, authentication requests. Also XMLRPC options requests.
35
	 * Fallback XMLRPC is also a bridge, but probably can be a class that inherits
36
	 * this one. Among other things it should strip existing methods.
37
	 *
38
	 * @param Array $methods an array of API method names for the Connection to accept and
39
	 *                       pass on to existing callables. It's possible to specify whether
40
	 *                       each method should be available for unauthenticated calls or not.
41
	 * @see Jetpack::__construct
42
	 */
43
	public function initialize( $methods ) {
44
		$methods;
45
	}
46
47
	/**
48
	 * Returns true if the current site is connected to WordPress.com.
49
	 *
50
	 * @return Boolean is the site connected?
51
	 */
52
	public function is_active() {
53
		return (bool) $this->get_access_token( self::JETPACK_MASTER_USER );
0 ignored issues
show
Documentation introduced by
self::JETPACK_MASTER_USER is of type boolean, but the function expects a false|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
54
	}
55
56
	/**
57
	 * Returns true if the user with the specified identifier is connected to
58
	 * WordPress.com.
59
	 *
60
	 * @param Integer $user_id the user identifier.
61
	 * @return Boolean is the user connected?
62
	 */
63
	public function is_user_connected( $user_id ) {
64
		return $user_id;
65
	}
66
67
	/**
68
	 * Get the wpcom user data of the current|specified connected user.
69
	 *
70
	 * @param Integer $user_id the user identifier.
71
	 * @return Object the user object.
72
	 */
73
	public function get_connected_user_data( $user_id ) {
74
		return $user_id;
75
	}
76
77
	/**
78
	 * Is the user the connection owner.
79
	 *
80
	 * @param Integer $user_id the user identifier.
81
	 * @return Boolean is the user the connection owner?
82
	 */
83
	public function is_connection_owner( $user_id ) {
84
		return $user_id;
85
	}
86
87
	/**
88
	 * Unlinks the current user from the linked WordPress.com user
89
	 *
90
	 * @param Integer $user_id the user identifier.
91
	 */
92
	public static function disconnect_user( $user_id ) {
93
		return $user_id;
94
	}
95
96
	/**
97
	 * Initializes a transport server, whatever it may be, saves into the object property.
98
	 * Should be changed to be protected.
99
	 */
100
	public function initialize_server() {
101
102
	}
103
104
	/**
105
	 * Checks if the current request is properly authenticated, bails if not.
106
	 * Should be changed to be protected.
107
	 */
108
	public function require_authentication() {
109
110
	}
111
112
	/**
113
	 * Verifies the correctness of the request signature.
114
	 * Should be changed to be protected.
115
	 */
116
	public function verify_signature() {
117
118
	}
119
120
	/**
121
	 * Returns the requested Jetpack API URL.
122
	 *
123
	 * @param String $relative_url the relative API path.
124
	 * @return String API URL.
125
	 */
126
	public function api_url( $relative_url ) {
127
128
		// TODO: rely on constants to override the default
129
		return rtrim( 'https://jetpack.wordpress.com/jetpack.' . $relative_url, '/\\' ) . '/1/';
130
	}
131
132
	/**
133
	 * Attempts Jetpack registration which sets up the site for connection. Should
134
	 * remain public because the call to action comes from the current site, not from
135
	 * WordPress.com.
136
	 *
137
	 * @return Integer zero on success, or a bitmask on failure.
138
	 */
139
	public function register() {
140
		// TODO: Tracking can't yet function without static Jetpack methods.
141
142
		add_action( 'pre_update_jetpack_option_register', array( '\Jetpack_Options', 'delete_option' ) );
143
		$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 );
144
145 View Code Duplication
		if (
146
			empty( $secrets['secret_1'] ) ||
147
			empty( $secrets['secret_2'] ) ||
148
			empty( $secrets['exp'] )
149
		) {
150
			return new \WP_Error( 'missing_secrets' );
151
		}
152
153
		// better to try (and fail) to set a higher timeout than this system
154
		// supports than to have register fail for more users than it should
155
		$timeout = $this->set_min_time_limit( 60 ) / 2;
156
157
		$gmt_offset = get_option( 'gmt_offset' );
158
		if ( ! $gmt_offset ) {
159
			$gmt_offset = 0;
160
		}
161
162
		$stats_options = get_option( 'stats_options' );
163
		$stats_id      = isset( $stats_options['blog_id'] )
164
				  ? $stats_options['blog_id']
165
				  : null;
166
167
		$args = array(
168
			'method'  => 'POST',
169
			'body'    => array(
170
				'siteurl'         => site_url(),
171
				'home'            => home_url(),
172
				'gmt_offset'      => $gmt_offset,
173
				'timezone_string' => (string) get_option( 'timezone_string' ),
174
				'site_name'       => (string) get_option( 'blogname' ),
175
				'secret_1'        => $secrets['secret_1'],
176
				'secret_2'        => $secrets['secret_2'],
177
				'site_lang'       => get_locale(),
178
				'timeout'         => $timeout,
179
				'stats_id'        => $stats_id,
180
				'state'           => get_current_user_id(),
181
				'site_created'    => $this->get_assumed_site_creation_date(),
182
				'jetpack_version' => JETPACK__VERSION,
183
			),
184
			'headers' => array(
185
				'Accept' => 'application/json',
186
			),
187
			'timeout' => $timeout,
188
		);
189
190
		$args['body'] = $this->apply_activation_source_to_args( $args['body'] );
191
192
		// TODO: fix URLs for bad hosts
193
		$response = \Jetpack_Client::_wp_remote_request(
194
			$this->api_url( 'register' ),
195
			$args,
196
			true
197
		);
198
199
		// Make sure the response is valid and does not contain any Jetpack errors
200
		$registration_details = $this->validate_remote_register_response( $response );
201
202
		if ( is_wp_error( $registration_details ) ) {
203
			return $registration_details;
204
		} elseif ( ! $registration_details ) {
205
			return new \WP_Error(
206
				'unknown_error',
207
				'',
208
				wp_remote_retrieve_response_code( $response )
209
			);
210
		}
211
212 View Code Duplication
		if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) {
213
			return new \WP_Error(
214
				'jetpack_secret',
215
				'',
216
				wp_remote_retrieve_response_code( $response )
217
			);
218
		}
219
220
		if ( isset( $registration_details->jetpack_public ) ) {
221
			$jetpack_public = (int) $registration_details->jetpack_public;
222
		} else {
223
			$jetpack_public = false;
224
		}
225
226
		\Jetpack_Options::update_options(
227
			array(
228
				'id'         => (int) $registration_details->jetpack_id,
229
				'blog_token' => (string) $registration_details->jetpack_secret,
230
				'public'     => $jetpack_public,
231
			)
232
		);
233
234
		/**
235
		 * Fires when a site is registered on WordPress.com.
236
		 *
237
		 * @since 3.7.0
238
		 *
239
		 * @param int $json->jetpack_id Jetpack Blog ID.
240
		 * @param string $json->jetpack_secret Jetpack Blog Token.
241
		 * @param int|bool $jetpack_public Is the site public.
242
		 */
243
		do_action(
244
			'jetpack_site_registered',
245
			$registration_details->jetpack_id,
246
			$registration_details->jetpack_secret,
247
			$jetpack_public
248
		);
249
250
		// TODO: Make jumpstart run on jetpack_site_registered action.
251
252
		return true;
253
	}
254
255
	/**
256
	 * Takes the response from the Jetpack register new site endpoint and
257
	 * verifies it worked properly.
258
	 *
259
	 * @since 2.6
260
	 * @return string|Jetpack_Error A JSON object on success or Jetpack_Error on failures
261
	 **/
262
	protected function validate_remote_register_response( $response ) {
263
		if ( is_wp_error( $response ) ) {
264
			return new \WP_Error(
265
				'register_http_request_failed',
266
				$response->get_error_message()
267
			);
268
		}
269
270
		$code   = wp_remote_retrieve_response_code( $response );
271
		$entity = wp_remote_retrieve_body( $response );
272
273
		if ( $entity ) {
274
			$registration_response = json_decode( $entity );
275
		} else {
276
			$registration_response = false;
277
		}
278
279
		$code_type = intval( $code / 100 );
280
		if ( 5 == $code_type ) {
281
			return new \WP_Error( 'wpcom_5??', $code );
282
		} elseif ( 408 == $code ) {
283
			return new \WP_Error( 'wpcom_408', $code );
284
		} elseif ( ! empty( $registration_response->error ) ) {
285
			if (
286
				'xml_rpc-32700' == $registration_response->error
287
				&& ! function_exists( 'xml_parser_create' )
288
			) {
289
				$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' );
290
			} else {
291
				$error_description = isset( $registration_response->error_description )
292
								   ? (string) $registration_response->error_description
293
								   : '';
294
			}
295
296
			return new \WP_Error(
297
				(string) $registration_response->error,
298
				$error_description,
299
				$code
300
			);
301
		} elseif ( 200 != $code ) {
302
			return new \WP_Error( 'wpcom_bad_response', $code );
303
		}
304
305
		// Jetpack ID error block
306 View Code Duplication
		if ( empty( $registration_response->jetpack_id ) ) {
307
			return new \WP_Error(
308
				'jetpack_id',
309
				sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
310
				$entity
311
			);
312
		} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) {
313
			return new \WP_Error(
314
				'jetpack_id',
315
				sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
316
				$entity
317
			);
318
		} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) {
319
			return new \WP_Error(
320
				'jetpack_id',
321
				sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ),
322
				$entity
323
			);
324
		}
325
326
		return $registration_response;
327
	}
328
329
	/**
330
	 * Builds the timeout limit for queries talking with the wpcom servers.
331
	 *
332
	 * Based on local php max_execution_time in php.ini
333
	 *
334
	 * @since 5.4
335
	 * @return int
336
	 **/
337
	public function get_max_execution_time() {
338
		$timeout = (int) ini_get( 'max_execution_time' );
339
340
		// Ensure exec time set in php.ini
341
		if ( ! $timeout ) {
342
			$timeout = 30;
343
		}
344
		return $timeout;
345
	}
346
347
	/**
348
	 * Sets a minimum request timeout, and returns the current timeout
349
	 *
350
	 * @since 5.4
351
	 **/
352 View Code Duplication
	public function set_min_time_limit( $min_timeout ) {
353
		$timeout = $this->get_max_execution_time();
354
		if ( $timeout < $min_timeout ) {
355
			$timeout = $min_timeout;
356
			set_time_limit( $timeout );
357
		}
358
		return $timeout;
359
	}
360
361
	/**
362
	 * Get our assumed site creation date.
363
	 * Calculated based on the earlier date of either:
364
	 * - Earliest admin user registration date.
365
	 * - Earliest date of post of any post type.
366
	 *
367
	 * @since 7.2.0
368
	 *
369
	 * @return string Assumed site creation date and time.
370
	 */
371 View Code Duplication
	public function get_assumed_site_creation_date() {
372
		$earliest_registered_users  = get_users(
373
			array(
374
				'role'    => 'administrator',
375
				'orderby' => 'user_registered',
376
				'order'   => 'ASC',
377
				'fields'  => array( 'user_registered' ),
378
				'number'  => 1,
379
			)
380
		);
381
		$earliest_registration_date = $earliest_registered_users[0]->user_registered;
382
383
		$earliest_posts = get_posts(
384
			array(
385
				'posts_per_page' => 1,
386
				'post_type'      => 'any',
387
				'post_status'    => 'any',
388
				'orderby'        => 'date',
389
				'order'          => 'ASC',
390
			)
391
		);
392
393
		// If there are no posts at all, we'll count only on user registration date.
394
		if ( $earliest_posts ) {
395
			$earliest_post_date = $earliest_posts[0]->post_date;
396
		} else {
397
			$earliest_post_date = PHP_INT_MAX;
398
		}
399
400
		return min( $earliest_registration_date, $earliest_post_date );
401
	}
402
403 View Code Duplication
	public static function apply_activation_source_to_args( $args ) {
404
		list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' );
405
406
		if ( $activation_source_name ) {
407
			$args['_as'] = urlencode( $activation_source_name );
408
		}
409
410
		if ( $activation_source_keyword ) {
411
			$args['_ak'] = urlencode( $activation_source_keyword );
412
		}
413
414
		return $args;
415
	}
416
417
	/**
418
	 * Returns the callable that would be used to generate secrets.
419
	 *
420
	 * @return Callable a function that returns a secure string to be used as a secret.
421
	 */
422
	protected function get_secret_callable() {
423
		if ( ! isset( $this->secret_callable ) ) {
424
			/**
425
			 * Allows modification of the callable that is used to generate connection secrets.
426
			 *
427
			 * @param Callable a function or method that returns a secret string.
428
			 */
429
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
430
		}
431
432
		return $this->secret_callable;
433
	}
434
435
	/**
436
	 * Generates two secret tokens and the end of life timestamp for them.
437
	 *
438
	 * @param String  $action  The action name.
439
	 * @param Integer $user_id The user identifier.
440
	 * @param Integer $exp     Expiration time in seconds.
441
	 */
442
	public function generate_secrets( $action, $user_id, $exp ) {
443
		$callable = $this->get_secret_callable();
444
445
		$secrets = \Jetpack_Options::get_raw_option(
446
			self::SECRETS_OPTION_NAME,
447
			array()
448
		);
449
450
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
451
452
		if (
453
			isset( $secrets[ $secret_name ] ) &&
454
			$secrets[ $secret_name ]['exp'] > time()
455
		) {
456
			return $secrets[ $secret_name ];
457
		}
458
459
		$secret_value = array(
460
			'secret_1' => call_user_func( $callable ),
461
			'secret_2' => call_user_func( $callable ),
462
			'exp'      => time() + $exp,
463
		);
464
465
		$secrets[ $secret_name ] = $secret_value;
466
467
		\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
468
		return $secrets[ $secret_name ];
469
	}
470
471
	/**
472
	 * Returns two secret tokens and the end of life timestamp for them.
473
	 *
474
	 * @param String  $action  The action name.
475
	 * @param Integer $user_id The user identifier.
476
	 * @return string|array an array of secrets or an error string.
477
	 */
478
	public function get_secrets( $action, $user_id ) {
479
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
480
		$secrets     = \Jetpack_Options::get_raw_option(
481
			self::SECRETS_OPTION_NAME,
482
			array()
483
		);
484
485
		if ( ! isset( $secrets[ $secret_name ] ) ) {
486
			return self::SECRETS_MISSING;
487
		}
488
489
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
490
			$this->delete_secrets( $action, $user_id );
491
			return self::SECRETS_EXPIRED;
492
		}
493
494
		return $secrets[ $secret_name ];
495
	}
496
497
	/**
498
	 * Deletes secret tokens in case they, for example, have expired.
499
	 *
500
	 * @param String  $action  The action name.
501
	 * @param Integer $user_id The user identifier.
502
	 */
503
	public function delete_secrets( $action, $user_id ) {
504
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
505
		$secrets     = \Jetpack_Options::get_raw_option(
506
			self::SECRETS_OPTION_NAME,
507
			array()
508
		);
509
		if ( isset( $secrets[ $secret_name ] ) ) {
510
			unset( $secrets[ $secret_name ] );
511
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
512
		}
513
	}
514
515
	/**
516
	 * Responds to a WordPress.com call to register the current site.
517
	 * Should be changed to protected.
518
	 *
519
	 * @param array $registration_data Array of [ secret_1, user_id ].
520
	 */
521
	public function handle_registration( array $registration_data ) {
522
		list( $registration_secret_1, $registration_user_id ) = $registration_data;
523
		if ( empty( $registration_user_id ) ) {
524
			return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 );
525
		}
526
527
		return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id );
528
	}
529
530
	/**
531
	 * Verify a Previously Generated Secret.
532
	 *
533
	 * @param string $action   The type of secret to verify.
534
	 * @param string $secret_1 The secret string to compare to what is stored.
535
	 * @param int    $user_id  The user ID of the owner of the secret.
536
	 */
537
	protected function verify_secrets( $action, $secret_1, $user_id ) {
538
		$allowed_actions = array( 'register', 'authorize', 'publicize' );
539
		if ( ! in_array( $action, $allowed_actions, true ) ) {
540
			return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
541
		}
542
543
		$user = get_user_by( 'id', $user_id );
544
545
		/**
546
		 * We've begun verifying the previously generated secret.
547
		 *
548
		 * @since 7.5.0
549
		 *
550
		 * @param string   $action The type of secret to verify.
551
		 * @param \WP_User $user The user object.
552
		 */
553
		do_action( 'jetpack_verify_secrets_begin', $action, $user );
554
555
		$return_error = function( \WP_Error $error ) use ( $action, $user ) {
556
			/**
557
			 * Verifying of the previously generated secret has failed.
558
			 *
559
			 * @since 7.5.0
560
			 *
561
			 * @param string    $action  The type of secret to verify.
562
			 * @param \WP_User  $user The user object.
563
			 * @param \WP_Error $error The error object.
564
			 */
565
			do_action( 'jetpack_verify_secrets_fail', $action, $user, $error );
566
567
			return $error;
568
		};
569
570
		$stored_secrets = $this->get_secrets( $action, $user_id );
571
		$this->delete_secrets( $action, $user_id );
572
573
		if ( empty( $secret_1 ) ) {
574
			return $return_error(
575
				new \WP_Error(
576
					'verify_secret_1_missing',
577
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
578
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ),
579
					400
580
				)
581
			);
582
		} elseif ( ! is_string( $secret_1 ) ) {
583
			return $return_error(
584
				new \WP_Error(
585
					'verify_secret_1_malformed',
586
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
587
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ),
588
					400
589
				)
590
			);
591
		} elseif ( empty( $user_id ) ) {
592
			// $user_id is passed around during registration as "state".
593
			return $return_error(
594
				new \WP_Error(
595
					'state_missing',
596
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
597
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ),
598
					400
599
				)
600
			);
601
		} elseif ( ! ctype_digit( (string) $user_id ) ) {
602
			return $return_error(
603
				new \WP_Error(
604
					'verify_secret_1_malformed',
605
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
606
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ),
607
					400
608
				)
609
			);
610
		}
611
612
		if ( ! $stored_secrets ) {
613
			return $return_error(
614
				new \WP_Error(
615
					'verify_secrets_missing',
616
					__( 'Verification secrets not found', 'jetpack' ),
617
					400
618
				)
619
			);
620
		} elseif ( is_wp_error( $stored_secrets ) ) {
621
			$stored_secrets->add_data( 400 );
0 ignored issues
show
Bug introduced by
The method add_data cannot be called on $stored_secrets (of type string|array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
622
			return $return_error( $stored_secrets );
623
		} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
624
			return $return_error(
625
				new \WP_Error(
626
					'verify_secrets_incomplete',
627
					__( 'Verification secrets are incomplete', 'jetpack' ),
628
					400
629
				)
630
			);
631
		} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
632
			return $return_error(
633
				new \WP_Error(
634
					'verify_secrets_mismatch',
635
					__( 'Secret mismatch', 'jetpack' ),
636
					400
637
				)
638
			);
639
		}
640
641
		/**
642
		 * We've succeeded at verifying the previously generated secret.
643
		 *
644
		 * @since 7.5.0
645
		 *
646
		 * @param string   $action The type of secret to verify.
647
		 * @param \WP_User $user The user object.
648
		 */
649
		do_action( 'jetpack_verify_secrets_success', $action, $user );
650
651
		return $stored_secrets['secret_2'];
652
	}
653
654
	/**
655
	 * Responds to a WordPress.com call to authorize the current user.
656
	 * Should be changed to protected.
657
	 */
658
	public function handle_authorization() {
659
660
	}
661
662
	/**
663
	 * Builds a URL to the Jetpack connection auth page.
664
	 * This needs rethinking.
665
	 *
666
	 * @param bool        $raw If true, URL will not be escaped.
667
	 * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
668
	 *                              If string, will be a custom redirect.
669
	 * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
670
	 * @param bool        $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
671
	 *
672
	 * @return string Connect URL
673
	 */
674
	public function build_connect_url( $raw, $redirect, $from, $register ) {
675
		return array( $raw, $redirect, $from, $register );
676
	}
677
678
	/**
679
	 * Disconnects from the Jetpack servers.
680
	 * Forgets all connection details and tells the Jetpack servers to do the same.
681
	 */
682
	public function disconnect_site() {
683
684
	}
685
686
	/**
687
	 * The Base64 Encoding of the SHA1 Hash of the Input.
688
	 *
689
	 * @param string $text The string to hash.
690
	 * @return string
691
	 */
692
	public function sha1_base64( $text ) {
693
		return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
694
	}
695
696
	/**
697
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
698
	 *
699
	 * @param string $domain The domain to check.
700
	 *
701
	 * @return bool|WP_Error
702
	 */
703
	public function is_usable_domain( $domain ) {
704
705
		// If it's empty, just fail out.
706
		if ( ! $domain ) {
707
			return new \WP_Error(
708
				'fail_domain_empty',
709
				/* translators: %1$s is a domain name. */
710
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
711
			);
712
		}
713
714
		/**
715
		 * Skips the usuable domain check when connecting a site.
716
		 *
717
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
718
		 *
719
		 * @since 4.1.0
720
		 *
721
		 * @param bool If the check should be skipped. Default false.
722
		 */
723
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
724
			return true;
725
		}
726
727
		// None of the explicit localhosts.
728
		$forbidden_domains = array(
729
			'wordpress.com',
730
			'localhost',
731
			'localhost.localdomain',
732
			'127.0.0.1',
733
			'local.wordpress.test',         // VVV pattern.
734
			'local.wordpress-trunk.test',   // VVV pattern.
735
			'src.wordpress-develop.test',   // VVV pattern.
736
			'build.wordpress-develop.test', // VVV pattern.
737
		);
738 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
739
			return new \WP_Error(
740
				'fail_domain_forbidden',
741
				sprintf(
742
					/* translators: %1$s is a domain name. */
743
					__(
744
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
745
						'jetpack'
746
					),
747
					$domain
748
				)
749
			);
750
		}
751
752
		// No .test or .local domains.
753 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
754
			return new \WP_Error(
755
				'fail_domain_tld',
756
				sprintf(
757
					/* translators: %1$s is a domain name. */
758
					__(
759
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
760
						'jetpack'
761
					),
762
					$domain
763
				)
764
			);
765
		}
766
767
		// No WPCOM subdomains.
768 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
769
			return new \WP_Error(
770
				'fail_subdomain_wpcom',
771
				sprintf(
772
					/* translators: %1$s is a domain name. */
773
					__(
774
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
775
						'jetpack'
776
					),
777
					$domain
778
				)
779
			);
780
		}
781
782
		// If PHP was compiled without support for the Filter module (very edge case).
783
		if ( ! function_exists( 'filter_var' ) ) {
784
			// Just pass back true for now, and let wpcom sort it out.
785
			return true;
786
		}
787
788
		return true;
789
	}
790
791
	/**
792
	 * Gets the requested token.
793
	 *
794
	 * Tokens are one of two types:
795
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
796
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
797
	 *    are not associated with a user account. They represent the site's connection with
798
	 *    the Jetpack servers.
799
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
800
	 *
801
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
802
	 * token, and $private is a secret that should never be displayed anywhere or sent
803
	 * over the network; it's used only for signing things.
804
	 *
805
	 * Blog Tokens can be "Normal" or "Special".
806
	 * * Normal: The result of a normal connection flow. They look like
807
	 *   "{$random_string_1}.{$random_string_2}"
808
	 *   That is, $token_key and $private are both random strings.
809
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
810
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
811
	 *   constant (rare).
812
	 * * Special: A connection token for sites that have gone through an alternative
813
	 *   connection flow. They look like:
814
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
815
	 *   That is, $private is a random string and $token_key has a special structure with
816
	 *   lots of semicolons.
817
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
818
	 *   JETPACK_BLOG_TOKEN constant.
819
	 *
820
	 * In particular, note that Normal Blog Tokens never start with ";" and that
821
	 * Special Blog Tokens always do.
822
	 *
823
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
824
	 * order:
825
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
826
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
827
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
828
	 *
829
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
830
	 * @param string|false $token_key If provided, check that the token matches the provided input.
831
	 *
832
	 * @return object|false
833
	 */
834
	public function get_access_token( $user_id = false, $token_key = false ) {
835
		$possible_special_tokens = array();
836
		$possible_normal_tokens  = array();
837
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
838
839
		if ( $user_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user_id of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
840
			if ( ! $user_tokens ) {
841
				return false;
842
			}
843
			if ( self::JETPACK_MASTER_USER === $user_id ) {
844
				$user_id = \Jetpack_Options::get_option( 'master_user' );
845
				if ( ! $user_id ) {
846
					return false;
847
				}
848
			}
849
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
850
				return false;
851
			}
852
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
853
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
854
				return false;
855
			}
856
			if ( $user_token_chunks[2] !== (string) $user_id ) {
857
				return false;
858
			}
859
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
860
		} else {
861
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
862
			if ( $stored_blog_token ) {
863
				$possible_normal_tokens[] = $stored_blog_token;
864
			}
865
866
			$defined_tokens = Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
867
				? explode( ',', Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
868
				: array();
869
870
			foreach ( $defined_tokens as $defined_token ) {
871
				if ( ';' === $defined_token[0] ) {
872
					$possible_special_tokens[] = $defined_token;
873
				} else {
874
					$possible_normal_tokens[] = $defined_token;
875
				}
876
			}
877
		}
878
879
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
880
			$possible_tokens = $possible_normal_tokens;
881
		} else {
882
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
883
		}
884
885
		if ( ! $possible_tokens ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $possible_tokens of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
886
			return false;
887
		}
888
889
		$valid_token = false;
890
891
		if ( false === $token_key ) {
892
			// Use first token.
893
			$valid_token = $possible_tokens[0];
894
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
895
			// Use first normal token.
896
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
897
		} else {
898
			// Use the token matching $token_key or false if none.
899
			// Ensure we check the full key.
900
			$token_check = rtrim( $token_key, '.' ) . '.';
901
902
			foreach ( $possible_tokens as $possible_token ) {
903
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
904
					$valid_token = $possible_token;
905
					break;
906
				}
907
			}
908
		}
909
910
		if ( ! $valid_token ) {
911
			return false;
912
		}
913
914
		return (object) array(
915
			'secret'           => $valid_token,
916
			'external_user_id' => (int) $user_id,
917
		);
918
	}
919
}
920