Completed
Push — add/signature-error-reporting ( 906206...21e253 )
by
unknown
104:48 queued 94:49
created

Manager   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 484
Duplicated Lines 1.24 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 6
loc 484
rs 6.96
c 0
b 0
f 0
wmc 53
lcom 1
cbo 2

21 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 3 1
A is_active() 0 3 1
A is_user_connected() 0 3 1
A get_connected_user_data() 0 3 1
A is_connection_owner() 0 3 1
A disconnect_user() 0 3 1
A initialize_server() 0 3 1
A require_authentication() 0 3 1
A verify_signature() 0 3 1
A register() 0 3 1
A get_secret_callable() 0 12 2
A generate_secrets() 0 28 3
A get_secrets() 0 18 3
A delete_secrets() 0 11 2
A handle_registration() 0 3 1
A handle_authorization() 0 3 1
A build_connect_url() 0 3 1
A disconnect_site() 0 3 1
A sha1_base64() 0 3 1
B is_usable_domain() 6 87 7
F get_access_token() 0 85 21

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manager, and based on these observations, apply Extract Interface, too.

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
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 false;
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
	 * Attempts Jetpack registration which sets up the site for connection. Should
122
	 * remain public because the call to action comes from the current site, not from
123
	 * WordPress.com.
124
	 *
125
	 * @return Integer zero on success, or a bitmask on failure.
126
	 */
127
	public function register() {
128
		return 0;
129
	}
130
131
	/**
132
	 * Returns the callable that would be used to generate secrets.
133
	 *
134
	 * @return Callable a function that returns a secure string to be used as a secret.
135
	 */
136
	protected function get_secret_callable() {
137
		if ( ! isset( $this->secret_callable ) ) {
138
			/**
139
			 * Allows modification of the callable that is used to generate connection secrets.
140
			 *
141
			 * @param Callable a function or method that returns a secret string.
142
			 */
143
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
144
		}
145
146
		return $this->secret_callable;
147
	}
148
149
	/**
150
	 * Generates two secret tokens and the end of life timestamp for them.
151
	 *
152
	 * @param String  $action  The action name.
153
	 * @param Integer $user_id The user identifier.
154
	 * @param Integer $exp     Expiration time in seconds.
155
	 */
156
	public function generate_secrets( $action, $user_id, $exp ) {
157
		$callable = $this->get_secret_callable();
158
159
		$secrets = \Jetpack_Options::get_raw_option(
160
			self::SECRETS_OPTION_NAME,
161
			array()
162
		);
163
164
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
165
166
		if (
167
			isset( $secrets[ $secret_name ] ) &&
168
			$secrets[ $secret_name ]['exp'] > time()
169
		) {
170
			return $secrets[ $secret_name ];
171
		}
172
173
		$secret_value = array(
174
			'secret_1' => call_user_func( $callable ),
175
			'secret_2' => call_user_func( $callable ),
176
			'exp'      => time() + $exp,
177
		);
178
179
		$secrets[ $secret_name ] = $secret_value;
180
181
		\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
182
		return $secrets[ $secret_name ];
183
	}
184
185
	/**
186
	 * Returns two secret tokens and the end of life timestamp for them.
187
	 *
188
	 * @param String  $action  The action name.
189
	 * @param Integer $user_id The user identifier.
190
	 * @return string|array an array of secrets or an error string.
191
	 */
192
	public function get_secrets( $action, $user_id ) {
193
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
194
		$secrets     = \Jetpack_Options::get_raw_option(
195
			self::SECRETS_OPTION_NAME,
196
			array()
197
		);
198
199
		if ( ! isset( $secrets[ $secret_name ] ) ) {
200
			return self::SECRETS_MISSING;
201
		}
202
203
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
204
			$this->delete_secrets( $action, $user_id );
205
			return self::SECRETS_EXPIRED;
206
		}
207
208
		return $secrets[ $secret_name ];
209
	}
210
211
	/**
212
	 * Deletes secret tokens in case they, for example, have expired.
213
	 *
214
	 * @param String  $action  The action name.
215
	 * @param Integer $user_id The user identifier.
216
	 */
217
	public function delete_secrets( $action, $user_id ) {
218
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
219
		$secrets     = \Jetpack_Options::get_raw_option(
220
			self::SECRETS_OPTION_NAME,
221
			array()
222
		);
223
		if ( isset( $secrets[ $secret_name ] ) ) {
224
			unset( $secrets[ $secret_name ] );
225
			\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets );
226
		}
227
	}
228
229
	/**
230
	 * Responds to a WordPress.com call to register the current site.
231
	 * Should be changed to protected.
232
	 */
233
	public function handle_registration() {
234
235
	}
236
237
	/**
238
	 * Responds to a WordPress.com call to authorize the current user.
239
	 * Should be changed to protected.
240
	 */
241
	public function handle_authorization() {
242
243
	}
244
245
	/**
246
	 * Builds a URL to the Jetpack connection auth page.
247
	 * This needs rethinking.
248
	 *
249
	 * @param bool        $raw If true, URL will not be escaped.
250
	 * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
251
	 *                              If string, will be a custom redirect.
252
	 * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
253
	 * @param bool        $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
254
	 *
255
	 * @return string Connect URL
256
	 */
257
	public function build_connect_url( $raw, $redirect, $from, $register ) {
258
		return array( $raw, $redirect, $from, $register );
259
	}
260
261
	/**
262
	 * Disconnects from the Jetpack servers.
263
	 * Forgets all connection details and tells the Jetpack servers to do the same.
264
	 */
265
	public function disconnect_site() {
266
267
	}
268
269
	/**
270
	 * @param $text
271
	 * @return string
272
	 */
273
	function sha1_base64( $text ) {
274
		return base64_encode( sha1( $text, true ) );
275
	}
276
277
	/**
278
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
279
	 *
280
	 * @param string $domain The domain to check.
281
	 *
282
	 * @return bool|WP_Error
283
	 */
284
	public function is_usable_domain( $domain ) {
285
286
		// If it's empty, just fail out.
287
		if ( ! $domain ) {
288
			return new WP_Error(
289
				'fail_domain_empty',
290
				/* translators: %1$s is a domain name. */
291
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
292
			);
293
		}
294
295
		/**
296
		 * Skips the usuable domain check when connecting a site.
297
		 *
298
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
299
		 *
300
		 * @since 4.1.0
301
		 *
302
		 * @param bool If the check should be skipped. Default false.
303
		 */
304
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
305
			return true;
306
		}
307
308
		// None of the explicit localhosts.
309
		$forbidden_domains = array(
310
			'wordpress.com',
311
			'localhost',
312
			'localhost.localdomain',
313
			'127.0.0.1',
314
			'local.wordpress.test',         // VVV pattern.
315
			'local.wordpress-trunk.test',   // VVV pattern.
316
			'src.wordpress-develop.test',   // VVV pattern.
317
			'build.wordpress-develop.test', // VVV pattern.
318
		);
319
		if ( in_array( $domain, $forbidden_domains, true ) ) {
320
			return new WP_Error(
321
				'fail_domain_forbidden',
322
				sprintf(
323
					/* translators: %1$s is a domain name. */
324
					__(
325
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
326
						'jetpack'
327
					),
328
					$domain
329
				)
330
			);
331
		}
332
333
		// No .test or .local domains.
334 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
335
			return new WP_Error(
336
				'fail_domain_tld',
337
				sprintf(
338
					/* translators: %1$s is a domain name. */
339
					__(
340
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
341
						'jetpack'
342
					),
343
					$domain
344
				)
345
			);
346
		}
347
348
		// No WPCOM subdomains.
349 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
350
			return new WP_Error(
351
				'fail_subdomain_wpcom',
352
				sprintf(
353
					/* translators: %1$s is a domain name. */
354
					__(
355
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
356
						'jetpack'
357
					),
358
					$domain
359
				)
360
			);
361
		}
362
363
		// If PHP was compiled without support for the Filter module (very edge case).
364
		if ( ! function_exists( 'filter_var' ) ) {
365
			// Just pass back true for now, and let wpcom sort it out.
366
			return true;
367
		}
368
369
		return true;
370
	}
371
372
	/**
373
	 * Gets the requested token.
374
	 *
375
	 * Tokens are one of two types:
376
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
377
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
378
	 *    are not associated with a user account. They represent the site's connection with
379
	 *    the Jetpack servers.
380
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
381
	 *
382
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
383
	 * token, and $private is a secret that should never be displayed anywhere or sent
384
	 * over the network; it's used only for signing things.
385
	 *
386
	 * Blog Tokens can be "Normal" or "Special".
387
	 * * Normal: The result of a normal connection flow. They look like
388
	 *   "{$random_string_1}.{$random_string_2}"
389
	 *   That is, $token_key and $private are both random strings.
390
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
391
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
392
	 *   constant (rare).
393
	 * * Special: A connection token for sites that have gone through an alternative
394
	 *   connection flow. They look like:
395
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
396
	 *   That is, $private is a random string and $token_key has a special structure with
397
	 *   lots of semicolons.
398
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
399
	 *   JETPACK_BLOG_TOKEN constant.
400
	 *
401
	 * In particular, note that Normal Blog Tokens never start with ";" and that
402
	 * Special Blog Tokens always do.
403
	 *
404
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
405
	 * order:
406
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
407
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
408
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
409
	 *
410
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
411
	 * @param string|false $token_key If provided, check that the token matches the provided input.
412
	 *
413
	 * @return object|false
414
	 */
415
	public function get_access_token( $user_id = false, $token_key = false ) {
416
		$possible_special_tokens = array();
417
		$possible_normal_tokens  = array();
418
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
419
420
		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...
421
			if ( ! $user_tokens ) {
422
				return false;
423
			}
424
			if ( self::JETPACK_MASTER_USER === $user_id ) {
425
				$user_id = \Jetpack_Options::get_option( 'master_user' );
426
				if ( ! $user_id ) {
427
					return false;
428
				}
429
			}
430
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
431
				return false;
432
			}
433
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
434
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
435
				return false;
436
			}
437
			if ( $user_id != $user_token_chunks[2] ) {
438
				return false;
439
			}
440
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
441
		} else {
442
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
443
			if ( $stored_blog_token ) {
444
				$possible_normal_tokens[] = $stored_blog_token;
445
			}
446
447
			$defined_tokens = Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
448
				? explode( ',', Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
449
				: array();
450
451
			foreach ( $defined_tokens as $defined_token ) {
452
				if ( ';' === $defined_token[0] ) {
453
					$possible_special_tokens[] = $defined_token;
454
				} else {
455
					$possible_normal_tokens[] = $defined_token;
456
				}
457
			}
458
		}
459
460
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
461
			$possible_tokens = $possible_normal_tokens;
462
		} else {
463
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
464
		}
465
466
		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...
467
			return false;
468
		}
469
470
		$valid_token = false;
471
472
		if ( false === $token_key ) {
473
			// Use first token.
474
			$valid_token = $possible_tokens[0];
475
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
476
			// Use first normal token.
477
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
478
		} else {
479
			// Use the token matching $token_key or false if none.
480
			// Ensure we check the full key.
481
			$token_check = rtrim( $token_key, '.' ) . '.';
482
483
			foreach ( $possible_tokens as $possible_token ) {
484
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
485
					$valid_token = $possible_token;
486
					break;
487
				}
488
			}
489
		}
490
491
		if ( ! $valid_token ) {
492
			return false;
493
		}
494
495
		return (object) array(
496
			'secret'           => $valid_token,
497
			'external_user_id' => (int) $user_id,
498
		);
499
	}
500
}
501