Completed
Push — feature/jetpack-packages-2 ( 256561...6d51bf )
by
unknown
706:29 queued 699:24
created

Manager::is_usable_domain()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 87

Duplication

Lines 39
Ratio 44.83 %

Importance

Changes 0
Metric Value
cc 7
nc 7
nop 1
dl 39
loc 87
rs 7.3503
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Connection\Manager_Interface;
11
use Automattic\Jetpack\Constants;
12
use Automattic\Jetpack\Tracking\Manager as JetpackTracking;
13
14
/**
15
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
16
 * and Jetpack.
17
 */
18
class Manager implements Manager_Interface {
19
20
	const SECRETS_MISSING        = 'secrets_missing';
21
	const SECRETS_EXPIRED        = 'secrets_expired';
22
	const SECRETS_OPTION_NAME    = 'jetpack_secrets';
23
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
24
	const JETPACK_MASTER_USER    = true;
25
26
	/**
27
	 * The procedure that should be run to generate secrets.
28
	 *
29
	 * @var Callable
30
	 */
31
	protected $secret_callable;
32
33
	/**
34
	 * Initializes all needed hooks and request handlers. Handles API calls, upload
35
	 * requests, authentication requests. Also XMLRPC options requests.
36
	 * Fallback XMLRPC is also a bridge, but probably can be a class that inherits
37
	 * this one. Among other things it should strip existing methods.
38
	 *
39
	 * @param Array $methods an array of API method names for the Connection to accept and
40
	 *                       pass on to existing callables. It's possible to specify whether
41
	 *                       each method should be available for unauthenticated calls or not.
42
	 * @see Jetpack::__construct
43
	 */
44
	public function initialize( $methods ) {
45
		$methods;
46
	}
47
48
	/**
49
	 * Returns true if the current site is connected to WordPress.com.
50
	 *
51
	 * @return Boolean is the site connected?
52
	 */
53
	public function is_active() {
54
		return false;
55
	}
56
57
	/**
58
	 * Returns true if the user with the specified identifier is connected to
59
	 * WordPress.com.
60
	 *
61
	 * @param Integer $user_id the user identifier.
62
	 * @return Boolean is the user connected?
63
	 */
64
	public function is_user_connected( $user_id ) {
65
		return $user_id;
66
	}
67
68
	/**
69
	 * Get the wpcom user data of the current|specified connected user.
70
	 *
71
	 * @param Integer $user_id the user identifier.
72
	 * @return Object the user object.
73
	 */
74
	public function get_connected_user_data( $user_id ) {
75
		return $user_id;
76
	}
77
78
	/**
79
	 * Is the user the connection owner.
80
	 *
81
	 * @param Integer $user_id the user identifier.
82
	 * @return Boolean is the user the connection owner?
83
	 */
84
	public function is_connection_owner( $user_id ) {
85
		return $user_id;
86
	}
87
88
	/**
89
	 * Unlinks the current user from the linked WordPress.com user
90
	 *
91
	 * @param Integer $user_id the user identifier.
92
	 */
93
	public static function disconnect_user( $user_id ) {
94
		return $user_id;
95
	}
96
97
	/**
98
	 * Initializes a transport server, whatever it may be, saves into the object property.
99
	 * Should be changed to be protected.
100
	 */
101
	public function initialize_server() {
102
103
	}
104
105
	/**
106
	 * Checks if the current request is properly authenticated, bails if not.
107
	 * Should be changed to be protected.
108
	 */
109
	public function require_authentication() {
110
111
	}
112
113
	/**
114
	 * Verifies the correctness of the request signature.
115
	 * Should be changed to be protected.
116
	 */
117
	public function verify_signature() {
118
119
	}
120
121
	/**
122
	 * Attempts Jetpack registration which sets up the site for connection. Should
123
	 * remain public because the call to action comes from the current site, not from
124
	 * WordPress.com.
125
	 *
126
	 * @return Integer zero on success, or a bitmask on failure.
127
	 */
128
	public function register() {
129
		return 0;
130
	}
131
132
	/**
133
	 * Returns the callable that would be used to generate secrets.
134
	 *
135
	 * @return Callable a function that returns a secure string to be used as a secret.
136
	 */
137
	protected function get_secret_callable() {
138
		if ( ! isset( $this->secret_callable ) ) {
139
			/**
140
			 * Allows modification of the callable that is used to generate connection secrets.
141
			 *
142
			 * @param Callable a function or method that returns a secret string.
143
			 */
144
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
145
		}
146
147
		return $this->secret_callable;
148
	}
149
150
	/**
151
	 * Generates two secret tokens and the end of life timestamp for them.
152
	 *
153
	 * @param String  $action  The action name.
154
	 * @param Integer $user_id The user identifier.
155
	 * @param Integer $exp     Expiration time in seconds.
156
	 */
157
	public function generate_secrets( $action, $user_id, $exp ) {
158
		$callable = $this->get_secret_callable();
159
160
		$secrets = \Jetpack_Options::get_raw_option(
161
			self::SECRETS_OPTION_NAME,
162
			array()
163
		);
164
165
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
166
167
		if (
168
			isset( $secrets[ $secret_name ] ) &&
169
			$secrets[ $secret_name ]['exp'] > time()
170
		) {
171
			return $secrets[ $secret_name ];
172
		}
173
174
		$secret_value = array(
175
			'secret_1' => call_user_func( $callable ),
176
			'secret_2' => call_user_func( $callable ),
177
			'exp'      => time() + $exp,
178
		);
179
180
		$secrets[ $secret_name ] = $secret_value;
181
182
		\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
183
		return $secrets[ $secret_name ];
184
	}
185
186
	/**
187
	 * Returns two secret tokens and the end of life timestamp for them.
188
	 *
189
	 * @param String  $action  The action name.
190
	 * @param Integer $user_id The user identifier.
191
	 * @return string|array an array of secrets or an error string.
192
	 */
193
	public function get_secrets( $action, $user_id ) {
194
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
195
		$secrets     = \Jetpack_Options::get_raw_option(
196
			self::SECRETS_OPTION_NAME,
197
			array()
198
		);
199
200
		if ( ! isset( $secrets[ $secret_name ] ) ) {
201
			return self::SECRETS_MISSING;
202
		}
203
204
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
205
			$this->delete_secrets( $action, $user_id );
206
			return self::SECRETS_EXPIRED;
207
		}
208
209
		return $secrets[ $secret_name ];
210
	}
211
212
	/**
213
	 * Deletes secret tokens in case they, for example, have expired.
214
	 *
215
	 * @param String  $action  The action name.
216
	 * @param Integer $user_id The user identifier.
217
	 */
218
	public function delete_secrets( $action, $user_id ) {
219
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
220
		$secrets     = \Jetpack_Options::get_raw_option(
221
			self::SECRETS_OPTION_NAME,
222
			array()
223
		);
224
		if ( isset( $secrets[ $secret_name ] ) ) {
225
			unset( $secrets[ $secret_name ] );
226
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
227
		}
228
	}
229
230
	/**
231
	 * Responds to a WordPress.com call to register the current site.
232
	 * Should be changed to protected.
233
	 *
234
	 * @param array $registration_data Array of [ secret_1, user_id ].
235
	 */
236
	public function handle_registration( array $registration_data ) {
237
		list( $registration_secret_1, $registration_user_id ) = $registration_data;
238
		if ( empty( $registration_user_id ) ) {
239
			return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 );
240
		}
241
242
		return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id );
243
	}
244
245
	/**
246
	 * Verify a Previously Generated Secret.
247
	 *
248
	 * @param string $action   The type of secret to verify.
249
	 * @param string $secret_1 The secret string to compare to what is stored.
250
	 * @param int    $user_id  The user ID of the owner of the secret.
251
	 */
252
	protected function verify_secrets( $action, $secret_1, $user_id ) {
253
		$allowed_actions = array( 'register', 'authorize', 'publicize' );
254
		if ( ! in_array( $action, $allowed_actions, true ) ) {
255
			return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 );
256
		}
257
258
		$user = get_user_by( 'id', $user_id );
259
260
		JetpackTracking::record_user_event( "jpc_verify_{$action}_begin", array(), $user );
261
262
		$return_error = function( \WP_Error $error ) use ( $action, $user ) {
263
			JetpackTracking::record_user_event(
264
				"jpc_verify_{$action}_fail",
265
				array(
266
					'error_code'    => $error->get_error_code(),
267
					'error_message' => $error->get_error_message(),
268
				),
269
				$user
270
			);
271
272
			return $error;
273
		};
274
275
		$stored_secrets = $this->get_secrets( $action, $user_id );
276
		$this->delete_secrets( $action, $user_id );
277
278
		if ( empty( $secret_1 ) ) {
279
			return $return_error(
280
				new \WP_Error(
281
					'verify_secret_1_missing',
282
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
283
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ),
284
					400
285
				)
286
			);
287
		} elseif ( ! is_string( $secret_1 ) ) {
288
			return $return_error(
289
				new \WP_Error(
290
					'verify_secret_1_malformed',
291
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
292
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ),
293
					400
294
				)
295
			);
296
		} elseif ( empty( $user_id ) ) {
297
			// $user_id is passed around during registration as "state".
298
			return $return_error(
299
				new \WP_Error(
300
					'state_missing',
301
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
302
					sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ),
303
					400
304
				)
305
			);
306
		} elseif ( ! ctype_digit( (string) $user_id ) ) {
307
			return $return_error(
308
				new \WP_Error(
309
					'verify_secret_1_malformed',
310
					/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */
311
					sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ),
312
					400
313
				)
314
			);
315
		}
316
317
		if ( ! $stored_secrets ) {
318
			return $return_error(
319
				new \WP_Error(
320
					'verify_secrets_missing',
321
					__( 'Verification secrets not found', 'jetpack' ),
322
					400
323
				)
324
			);
325
		} elseif ( is_wp_error( $stored_secrets ) ) {
326
			$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...
327
			return $return_error( $stored_secrets );
328
		} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) {
329
			return $return_error(
330
				new \WP_Error(
331
					'verify_secrets_incomplete',
332
					__( 'Verification secrets are incomplete', 'jetpack' ),
333
					400
334
				)
335
			);
336
		} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) {
337
			return $return_error(
338
				new \WP_Error(
339
					'verify_secrets_mismatch',
340
					__( 'Secret mismatch', 'jetpack' ),
341
					400
342
				)
343
			);
344
		}
345
346
		JetpackTracking::record_user_event( "jpc_verify_{$action}_success", array(), $user );
347
348
		return $stored_secrets['secret_2'];
349
	}
350
351
	/**
352
	 * Responds to a WordPress.com call to authorize the current user.
353
	 * Should be changed to protected.
354
	 */
355
	public function handle_authorization() {
356
357
	}
358
359
	/**
360
	 * Builds a URL to the Jetpack connection auth page.
361
	 * This needs rethinking.
362
	 *
363
	 * @param bool        $raw If true, URL will not be escaped.
364
	 * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
365
	 *                              If string, will be a custom redirect.
366
	 * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
367
	 * @param bool        $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
368
	 *
369
	 * @return string Connect URL
370
	 */
371
	public function build_connect_url( $raw, $redirect, $from, $register ) {
372
		return array( $raw, $redirect, $from, $register );
373
	}
374
375
	/**
376
	 * Disconnects from the Jetpack servers.
377
	 * Forgets all connection details and tells the Jetpack servers to do the same.
378
	 */
379
	public function disconnect_site() {
380
381
	}
382
383
	/**
384
	 * The Base64 Encoding of the SHA1 Hash of the Input.
385
	 *
386
	 * @param string $text The string to hash.
387
	 * @return string
388
	 */
389
	public function sha1_base64( $text ) {
390
		return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
391
	}
392
393
	/**
394
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
395
	 *
396
	 * @param string $domain The domain to check.
397
	 *
398
	 * @return bool|WP_Error
399
	 */
400
	public function is_usable_domain( $domain ) {
401
402
		// If it's empty, just fail out.
403
		if ( ! $domain ) {
404
			return new \WP_Error(
405
				'fail_domain_empty',
406
				/* translators: %1$s is a domain name. */
407
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
408
			);
409
		}
410
411
		/**
412
		 * Skips the usuable domain check when connecting a site.
413
		 *
414
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
415
		 *
416
		 * @since 4.1.0
417
		 *
418
		 * @param bool If the check should be skipped. Default false.
419
		 */
420
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
421
			return true;
422
		}
423
424
		// None of the explicit localhosts.
425
		$forbidden_domains = array(
426
			'wordpress.com',
427
			'localhost',
428
			'localhost.localdomain',
429
			'127.0.0.1',
430
			'local.wordpress.test',         // VVV pattern.
431
			'local.wordpress-trunk.test',   // VVV pattern.
432
			'src.wordpress-develop.test',   // VVV pattern.
433
			'build.wordpress-develop.test', // VVV pattern.
434
		);
435 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
436
			return new \WP_Error(
437
				'fail_domain_forbidden',
438
				sprintf(
439
					/* translators: %1$s is a domain name. */
440
					__(
441
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
442
						'jetpack'
443
					),
444
					$domain
445
				)
446
			);
447
		}
448
449
		// No .test or .local domains.
450 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
451
			return new \WP_Error(
452
				'fail_domain_tld',
453
				sprintf(
454
					/* translators: %1$s is a domain name. */
455
					__(
456
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
457
						'jetpack'
458
					),
459
					$domain
460
				)
461
			);
462
		}
463
464
		// No WPCOM subdomains.
465 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
466
			return new \WP_Error(
467
				'fail_subdomain_wpcom',
468
				sprintf(
469
					/* translators: %1$s is a domain name. */
470
					__(
471
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
472
						'jetpack'
473
					),
474
					$domain
475
				)
476
			);
477
		}
478
479
		// If PHP was compiled without support for the Filter module (very edge case).
480
		if ( ! function_exists( 'filter_var' ) ) {
481
			// Just pass back true for now, and let wpcom sort it out.
482
			return true;
483
		}
484
485
		return true;
486
	}
487
488
	/**
489
	 * Gets the requested token.
490
	 *
491
	 * Tokens are one of two types:
492
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
493
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
494
	 *    are not associated with a user account. They represent the site's connection with
495
	 *    the Jetpack servers.
496
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
497
	 *
498
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
499
	 * token, and $private is a secret that should never be displayed anywhere or sent
500
	 * over the network; it's used only for signing things.
501
	 *
502
	 * Blog Tokens can be "Normal" or "Special".
503
	 * * Normal: The result of a normal connection flow. They look like
504
	 *   "{$random_string_1}.{$random_string_2}"
505
	 *   That is, $token_key and $private are both random strings.
506
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
507
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
508
	 *   constant (rare).
509
	 * * Special: A connection token for sites that have gone through an alternative
510
	 *   connection flow. They look like:
511
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
512
	 *   That is, $private is a random string and $token_key has a special structure with
513
	 *   lots of semicolons.
514
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
515
	 *   JETPACK_BLOG_TOKEN constant.
516
	 *
517
	 * In particular, note that Normal Blog Tokens never start with ";" and that
518
	 * Special Blog Tokens always do.
519
	 *
520
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
521
	 * order:
522
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
523
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
524
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
525
	 *
526
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
527
	 * @param string|false $token_key If provided, check that the token matches the provided input.
528
	 *
529
	 * @return object|false
530
	 */
531
	public function get_access_token( $user_id = false, $token_key = false ) {
532
		$possible_special_tokens = array();
533
		$possible_normal_tokens  = array();
534
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
535
536
		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...
537
			if ( ! $user_tokens ) {
538
				return false;
539
			}
540
			if ( self::JETPACK_MASTER_USER === $user_id ) {
541
				$user_id = \Jetpack_Options::get_option( 'master_user' );
542
				if ( ! $user_id ) {
543
					return false;
544
				}
545
			}
546
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
547
				return false;
548
			}
549
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
550
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
551
				return false;
552
			}
553
			if ( $user_token_chunks[2] !== (string) $user_id ) {
554
				return false;
555
			}
556
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
557
		} else {
558
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
559
			if ( $stored_blog_token ) {
560
				$possible_normal_tokens[] = $stored_blog_token;
561
			}
562
563
			$defined_tokens = Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
564
				? explode( ',', Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
565
				: array();
566
567
			foreach ( $defined_tokens as $defined_token ) {
568
				if ( ';' === $defined_token[0] ) {
569
					$possible_special_tokens[] = $defined_token;
570
				} else {
571
					$possible_normal_tokens[] = $defined_token;
572
				}
573
			}
574
		}
575
576
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
577
			$possible_tokens = $possible_normal_tokens;
578
		} else {
579
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
580
		}
581
582
		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...
583
			return false;
584
		}
585
586
		$valid_token = false;
587
588
		if ( false === $token_key ) {
589
			// Use first token.
590
			$valid_token = $possible_tokens[0];
591
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
592
			// Use first normal token.
593
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
594
		} else {
595
			// Use the token matching $token_key or false if none.
596
			// Ensure we check the full key.
597
			$token_check = rtrim( $token_key, '.' ) . '.';
598
599
			foreach ( $possible_tokens as $possible_token ) {
600
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
601
					$valid_token = $possible_token;
602
					break;
603
				}
604
			}
605
		}
606
607
		if ( ! $valid_token ) {
608
			return false;
609
		}
610
611
		return (object) array(
612
			'secret'           => $valid_token,
613
			'external_user_id' => (int) $user_id,
614
		);
615
	}
616
}
617