Completed
Push — add/signature-error-reporting ( 21e253...95a087 )
by
unknown
07:25
created

Manager::is_user_connected()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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\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
	 * Returns a base64-encoded sha1 hash of some text
271
	 *
272
	 * @param string $text The text to hash.
273
	 * @return string
274
	 */
275
	public function sha1_base64( $text ) {
276
		return base64_encode( sha1( $text, true ) );
277
	}
278
279
	/**
280
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
281
	 *
282
	 * @param string $domain The domain to check.
283
	 *
284
	 * @return bool|WP_Error
285
	 */
286
	public function is_usable_domain( $domain ) {
287
288
		// If it's empty, just fail out.
289
		if ( ! $domain ) {
290
			return new \WP_Error(
291
				'fail_domain_empty',
292
				/* translators: %1$s is a domain name. */
293
				sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain )
294
			);
295
		}
296
297
		/**
298
		 * Skips the usuable domain check when connecting a site.
299
		 *
300
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
301
		 *
302
		 * @since 4.1.0
303
		 *
304
		 * @param bool If the check should be skipped. Default false.
305
		 */
306
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
307
			return true;
308
		}
309
310
		// None of the explicit localhosts.
311
		$forbidden_domains = array(
312
			'wordpress.com',
313
			'localhost',
314
			'localhost.localdomain',
315
			'127.0.0.1',
316
			'local.wordpress.test',         // VVV pattern.
317
			'local.wordpress-trunk.test',   // VVV pattern.
318
			'src.wordpress-develop.test',   // VVV pattern.
319
			'build.wordpress-develop.test', // VVV pattern.
320
		);
321 View Code Duplication
		if ( in_array( $domain, $forbidden_domains, true ) ) {
322
			return new \WP_Error(
323
				'fail_domain_forbidden',
324
				sprintf(
325
					/* translators: %1$s is a domain name. */
326
					__(
327
						'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.',
328
						'jetpack'
329
					),
330
					$domain
331
				)
332
			);
333
		}
334
335
		// No .test or .local domains.
336 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
337
			return new \WP_Error(
338
				'fail_domain_tld',
339
				sprintf(
340
					/* translators: %1$s is a domain name. */
341
					__(
342
						'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.',
343
						'jetpack'
344
					),
345
					$domain
346
				)
347
			);
348
		}
349
350
		// No WPCOM subdomains.
351 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
352
			return new \WP_Error(
353
				'fail_subdomain_wpcom',
354
				sprintf(
355
					/* translators: %1$s is a domain name. */
356
					__(
357
						'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.',
358
						'jetpack'
359
					),
360
					$domain
361
				)
362
			);
363
		}
364
365
		// If PHP was compiled without support for the Filter module (very edge case).
366
		if ( ! function_exists( 'filter_var' ) ) {
367
			// Just pass back true for now, and let wpcom sort it out.
368
			return true;
369
		}
370
371
		return true;
372
	}
373
374
	/**
375
	 * Gets the requested token.
376
	 *
377
	 * Tokens are one of two types:
378
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
379
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
380
	 *    are not associated with a user account. They represent the site's connection with
381
	 *    the Jetpack servers.
382
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
383
	 *
384
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
385
	 * token, and $private is a secret that should never be displayed anywhere or sent
386
	 * over the network; it's used only for signing things.
387
	 *
388
	 * Blog Tokens can be "Normal" or "Special".
389
	 * * Normal: The result of a normal connection flow. They look like
390
	 *   "{$random_string_1}.{$random_string_2}"
391
	 *   That is, $token_key and $private are both random strings.
392
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
393
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
394
	 *   constant (rare).
395
	 * * Special: A connection token for sites that have gone through an alternative
396
	 *   connection flow. They look like:
397
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
398
	 *   That is, $private is a random string and $token_key has a special structure with
399
	 *   lots of semicolons.
400
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
401
	 *   JETPACK_BLOG_TOKEN constant.
402
	 *
403
	 * In particular, note that Normal Blog Tokens never start with ";" and that
404
	 * Special Blog Tokens always do.
405
	 *
406
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
407
	 * order:
408
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
409
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
410
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
411
	 *
412
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
413
	 * @param string|false $token_key If provided, check that the token matches the provided input.
414
	 * @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.
415
	 *
416
	 * @return object|false
417
	 */
418
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
419
		$possible_special_tokens = array();
420
		$possible_normal_tokens  = array();
421
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
422
423
		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...
424
			if ( ! $user_tokens ) {
425
				return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' );
426
			}
427
			if ( self::JETPACK_MASTER_USER === $user_id ) {
428
				$user_id = \Jetpack_Options::get_option( 'master_user' );
429
				if ( ! $user_id ) {
430
					return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' );
431
				}
432
			}
433
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
434
				return $suppress_errors ? false : new \WP_Error( 'no_token_for_user' );
435
			}
436
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
437
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
438
				return $suppress_errors ? false : new \WP_Error( 'token_missing_two_periods' );
439
			}
440
			if ( $user_id != $user_token_chunks[2] ) {
441
				return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch' );
442
			}
443
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
444
		} else {
445
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
446
			if ( $stored_blog_token ) {
447
				$possible_normal_tokens[] = $stored_blog_token;
448
			}
449
450
			$defined_tokens = Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
451
				? explode( ',', Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
452
				: array();
453
454
			foreach ( $defined_tokens as $defined_token ) {
455
				if ( ';' === $defined_token[0] ) {
456
					$possible_special_tokens[] = $defined_token;
457
				} else {
458
					$possible_normal_tokens[] = $defined_token;
459
				}
460
			}
461
		}
462
463
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
464
			$possible_tokens = $possible_normal_tokens;
465
		} else {
466
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
467
		}
468
469
		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...
470
			return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' );
471
		}
472
473
		$valid_token = false;
474
475
		if ( false === $token_key ) {
476
			// Use first token.
477
			$valid_token = $possible_tokens[0];
478
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
479
			// Use first normal token.
480
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
481
		} else {
482
			// Use the token matching $token_key or false if none.
483
			// Ensure we check the full key.
484
			$token_check = rtrim( $token_key, '.' ) . '.';
485
486
			foreach ( $possible_tokens as $possible_token ) {
487
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
488
					$valid_token = $possible_token;
489
					break;
490
				}
491
			}
492
		}
493
494
		if ( ! $valid_token ) {
495
			return $suppress_errors ? false : new \WP_Error( 'no_valid_token' );
496
		}
497
498
		return (object) array(
499
			'secret'           => $valid_token,
500
			'external_user_id' => (int) $user_id,
501
		);
502
	}
503
}
504