Completed
Push — add/is-active-to-connection-pa... ( 87b386...4d62cd )
by
unknown
199:53 queued 192:39
created

Manager::get_magic_normal_token_key()   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 0
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\Options\Manager as Options_Manager;
11
12
/**
13
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
14
 * and Jetpack.
15
 */
16
class Manager implements Manager_Interface {
17
	/*
18
	 * Used internally when we want to look for the Normal Blog Token
19
	 * without knowing its token key ahead of time.
20
	 */
21
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
22
	const SECRETS_MISSING        = 'secrets_missing';
23
	const SECRETS_EXPIRED        = 'secrets_expired';
24
	const SECRETS_OPTION_NAME    = 'secrets';
25
	const MASTER_USER            = true;
26
27
	/**
28
	 * The object for managing options.
29
	 *
30
	 * @var \Automattic\Jetpack\Options\Manager
31
	 */
32
	protected $option_manager;
33
34
	/**
35
	 * The procedure that should be run to generate secrets.
36
	 *
37
	 * @var Callable
38
	 */
39
	protected $secret_callable;
40
41
	/**
42
	 * Initializes all needed hooks and request handlers. Handles API calls, upload
43
	 * requests, authentication requests. Also XMLRPC options requests.
44
	 * Fallback XMLRPC is also a bridge, but probably can be a class that inherits
45
	 * this one. Among other things it should strip existing methods.
46
	 *
47
	 * @param Array $methods an array of API method names for the Connection to accept and
48
	 *                       pass on to existing callables. It's possible to specify whether
49
	 *                       each method should be available for unauthenticated calls or not.
50
	 * @see Jetpack::__construct
51
	 */
52
	public function initialize( $methods ) {
53
		$methods;
54
	}
55
56
	/**
57
	 * Returns true if the current site is connected to WordPress.com.
58
	 *
59
	 * @return Boolean is the site connected?
60
	 */
61
	public function is_active() {
62
		return (bool) $this->get_access_token( self::MASTER_USER );
0 ignored issues
show
Documentation introduced by
self::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...
63
	}
64
65
	public function get_magic_normal_token_key() {
66
		return self::MAGIC_NORMAL_TOKEN_KEY;
67
	}
68
69
	/**
70
	 * Returns true if the user with the specified identifier is connected to
71
	 * WordPress.com.
72
	 *
73
	 * @param Integer $user_id the user identifier.
74
	 * @return Boolean is the user connected?
75
	 */
76
	public function is_user_connected( $user_id ) {
77
		$user_id = false === $user_id ? get_current_user_id() : absint( $user_id );
78
		if ( ! $user_id ) {
79
			return false;
80
		}
81
82
		return (bool) $this->get_access_token( $user_id );
83
	}
84
85
	/**
86
	 * Get the wpcom user data of the current|specified connected user.
87
	 *
88
	 * @param Integer $user_id the user identifier.
89
	 * @return Object the user object.
90
	 */
91
	public function get_connected_user_data( $user_id ) {
92
		return $user_id;
93
	}
94
95
	/**
96
	 * Is the user the connection owner.
97
	 *
98
	 * @param Integer $user_id the user identifier.
99
	 * @return Boolean is the user the connection owner?
100
	 */
101
	public function is_connection_owner( $user_id ) {
102
		return $user_id;
103
	}
104
105
	/**
106
	 * Unlinks the current user from the linked WordPress.com user
107
	 *
108
	 * @param Integer $user_id the user identifier.
109
	 */
110
	public static function disconnect_user( $user_id ) {
111
		return $user_id;
112
	}
113
114
	/**
115
	 * Initializes a transport server, whatever it may be, saves into the object property.
116
	 * Should be changed to be protected.
117
	 */
118
	public function initialize_server() {
119
120
	}
121
122
	/**
123
	 * Checks if the current request is properly authenticated, bails if not.
124
	 * Should be changed to be protected.
125
	 */
126
	public function require_authentication() {
127
128
	}
129
130
	/**
131
	 * Verifies the correctness of the request signature.
132
	 * Should be changed to be protected.
133
	 */
134
	public function verify_signature() {
135
136
	}
137
138
	/**
139
	 * Attempts Jetpack registration which sets up the site for connection. Should
140
	 * remain public because the call to action comes from the current site, not from
141
	 * WordPress.com.
142
	 *
143
	 * @return Integer zero on success, or a bitmask on failure.
144
	 */
145
	public function register() {
146
		return 0;
147
	}
148
149
	/**
150
	 * Returns the callable that would be used to generate secrets.
151
	 *
152
	 * @return Callable a function that returns a secure string to be used as a secret.
153
	 */
154
	protected function get_secret_callable() {
155
		if ( ! isset( $this->secret_callable ) ) {
156
			/**
157
			 * Allows modification of the callable that is used to generate connection secrets.
158
			 *
159
			 * @param Callable a function or method that returns a secret string.
160
			 */
161
			$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' );
162
		}
163
164
		return $this->secret_callable;
165
	}
166
167
	/**
168
	 * Returns the object that is to be used for all option manipulation.
169
	 *
170
	 * @return Object $manager an option manager object.
171
	 */
172
	protected function get_option_manager() {
173
		if ( ! isset( $this->option_manager ) ) {
174
			/**
175
			 * Allows modification of the object that is used to manipulate stored data.
176
			 *
177
			 * @param Jetpack_Options an option manager object.
178
			 */
179
			$this->option_manager = apply_filters( 'jetpack_connection_option_manager', new Options_Manager() );
180
		}
181
182
		return $this->option_manager;
183
	}
184
185
	/**
186
	 * Generates 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
	 * @param Integer $exp     Expiration time in seconds.
191
	 */
192
	public function generate_secrets( $action, $user_id, $exp ) {
193
		$callable = $this->get_secret_callable();
194
195
		$secrets = $this->get_option_manager()->get_raw_option( 'jetpack_secrets', array() );
196
197
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
198
199
		if (
200
			isset( $secrets[ $secret_name ] ) &&
201
			$secrets[ $secret_name ]['exp'] > time()
202
		) {
203
			return $secrets[ $secret_name ];
204
		}
205
206
		$secret_value = array(
207
			'secret_1' => call_user_func( $callable ),
208
			'secret_2' => call_user_func( $callable ),
209
			'exp'      => time() + $exp,
210
		);
211
212
		$secrets[ $secret_name ] = $secret_value;
213
214
		$this->get_option_manager()->update_option( self::SECRETS_OPTION_NAME, $secrets );
215
		return $secrets[ $secret_name ];
216
	}
217
218
	/**
219
	 * Returns two secret tokens and the end of life timestamp for them.
220
	 *
221
	 * @param String  $action  The action name.
222
	 * @param Integer $user_id The user identifier.
223
	 * @return string|array an array of secrets or an error string.
224
	 */
225
	public function get_secrets( $action, $user_id ) {
226
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
227
		$secrets     = $this->get_option_manager()->get_option( self::SECRETS_OPTION_NAME, array() );
228
229
		if ( ! isset( $secrets[ $secret_name ] ) ) {
230
			return self::SECRETS_MISSING;
231
		}
232
233
		if ( $secrets[ $secret_name ]['exp'] < time() ) {
234
			$this->delete_secrets( $action, $user_id );
235
			return self::SECRETS_EXPIRED;
236
		}
237
238
		return $secrets[ $secret_name ];
239
	}
240
241
	/**
242
	 * Deletes secret tokens in case they, for example, have expired.
243
	 *
244
	 * @param String  $action  The action name.
245
	 * @param Integer $user_id The user identifier.
246
	 */
247
	public function delete_secrets( $action, $user_id ) {
248
		$secret_name = 'jetpack_' . $action . '_' . $user_id;
249
		$secrets     = $this->get_option_manager()->get_option( self::SECRETS_OPTION_NAME, array() );
250
		if ( isset( $secrets[ $secret_name ] ) ) {
251
			unset( $secrets[ $secret_name ] );
252
			$this->get_option_manager()->update_option( self::SECRETS_OPTION_NAME, $secrets );
253
		}
254
	}
255
256
	/**
257
	 * Responds to a WordPress.com call to register the current site.
258
	 * Should be changed to protected.
259
	 */
260
	public function handle_registration() {
261
262
	}
263
264
	/**
265
	 * Responds to a WordPress.com call to authorize the current user.
266
	 * Should be changed to protected.
267
	 */
268
	public function handle_authorization() {
269
270
	}
271
272
	/**
273
	 * Builds a URL to the Jetpack connection auth page.
274
	 * This needs rethinking.
275
	 *
276
	 * @param bool        $raw If true, URL will not be escaped.
277
	 * @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection.
278
	 *                              If string, will be a custom redirect.
279
	 * @param bool|string $from If not false, adds 'from=$from' param to the connect URL.
280
	 * @param bool        $register If true, will generate a register URL regardless of the existing token, since 4.9.0.
281
	 *
282
	 * @return string Connect URL
283
	 */
284
	public function build_connect_url( $raw, $redirect, $from, $register ) {
285
		return array( $raw, $redirect, $from, $register );
286
	}
287
288
	/**
289
	 * Disconnects from the Jetpack servers.
290
	 * Forgets all connection details and tells the Jetpack servers to do the same.
291
	 */
292
	public function disconnect_site() {
293
294
	}
295
296
	/**
297
	 * Gets the requested token.
298
	 *
299
	 * Tokens are one of two types:
300
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
301
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
302
	 *    are not associated with a user account. They represent the site's connection with
303
	 *    the Jetpack servers.
304
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
305
	 *
306
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
307
	 * token, and $private is a secret that should never be displayed anywhere or sent
308
	 * over the network; it's used only for signing things.
309
	 *
310
	 * Blog Tokens can be "Normal" or "Special".
311
	 * * Normal: The result of a normal connection flow. They look like
312
	 *   "{$random_string_1}.{$random_string_2}"
313
	 *   That is, $token_key and $private are both random strings.
314
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
315
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
316
	 *   constant (rare).
317
	 * * Special: A connection token for sites that have gone through an alternative
318
	 *   connection flow. They look like:
319
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
320
	 *   That is, $private is a random string and $token_key has a special structure with
321
	 *   lots of semicolons.
322
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
323
	 *   JETPACK_BLOG_TOKEN constant.
324
	 *
325
	 * In particular, note that Normal Blog Tokens never start with ";" and that
326
	 * Special Blog Tokens always do.
327
	 *
328
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
329
	 * order:
330
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
331
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
332
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
333
	 *
334
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
335
	 * @param string|false $token_key If provided, check that the token matches the provided input.
336
	 *                                false                                : Use first token. Default.
337
	 *                                Jetpack_Data::MAGIC_NORMAL_TOKEN_KEY : Use first Normal Token.
338
	 *                                non-empty string                     : Use matching token
339
	 * @return object|false
340
	 */
341
	public function get_access_token( $user_id = false, $token_key = false ) {
342
		$possible_special_tokens = array();
343
		$possible_normal_tokens  = array();
344
345
		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...
346
			if ( ! $user_tokens = $this->get_option_manager()->get_option( 'user_tokens' ) ) {
347
				return false;
348
			}
349
			if ( self::MASTER_USER === $user_id ) {
350
				if ( ! $user_id = $this->get_option_manager()->get_option( 'master_user' ) ) {
351
					return false;
352
				}
353
			}
354
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
355
				return false;
356
			}
357
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
358
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
359
				return false;
360
			}
361
			if ( $user_id != $user_token_chunks[2] ) {
362
				return false;
363
			}
364
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
365
		} else {
366
			$stored_blog_token = $this->get_option_manager()->get_option( 'blog_token' );
367
			if ( $stored_blog_token ) {
368
				$possible_normal_tokens[] = $stored_blog_token;
369
			}
370
371
			$defined_tokens = \Jetpack_Constants::is_defined( 'JETPACK_BLOG_TOKEN' )
372
				? explode( ',', \Jetpack_Constants::get_constant( 'JETPACK_BLOG_TOKEN' ) )
373
				: array();
374
375
			foreach ( $defined_tokens as $defined_token ) {
376
				if ( ';' === $defined_token[0] ) {
377
					$possible_special_tokens[] = $defined_token;
378
				} else {
379
					$possible_normal_tokens[] = $defined_token;
380
				}
381
			}
382
		}
383
384
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
385
			$possible_tokens = $possible_normal_tokens;
386
		} else {
387
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
388
		}
389
390
		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...
391
			return false;
392
		}
393
394
		$valid_token = false;
395
396
		if ( false === $token_key ) {
397
			// Use first token.
398
			$valid_token = $possible_tokens[0];
399
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
400
			// Use first normal token.
401
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
402
		} else {
403
			// Use the token matching $token_key or false if none.
404
			// Ensure we check the full key.
405
			$token_check = rtrim( $token_key, '.' ) . '.';
406
407
			foreach ( $possible_tokens as $possible_token ) {
408
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
409
					$valid_token = $possible_token;
410
					break;
411
				}
412
			}
413
		}
414
415
		if ( ! $valid_token ) {
416
			return false;
417
		}
418
419
		return (object) array(
420
			'secret'           => $valid_token,
421
			'external_user_id' => (int) $user_id,
422
		);
423
	}
424
425
	/**
426
	 * This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase.
427
	 *
428
	 * @param $domain
429
	 * @param array  $extra
430
	 *
431
	 * @return bool|WP_Error
432
	 */
433
	public function is_usable_domain( $domain, $extra = array() ) {
434
435
		// If it's empty, just fail out.
436
		if ( ! $domain ) {
437
			return new WP_Error( 'fail_domain_empty', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) );
438
		}
439
440
		/**
441
		 * Skips the usuable domain check when connecting a site.
442
		 *
443
		 * Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com
444
		 *
445
		 * @since 4.1.0
446
		 *
447
		 * @param bool If the check should be skipped. Default false.
448
		 */
449
		if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) {
450
			return true;
451
		}
452
453
		// None of the explicit localhosts.
454
		$forbidden_domains = array(
455
			'wordpress.com',
456
			'localhost',
457
			'localhost.localdomain',
458
			'127.0.0.1',
459
			'local.wordpress.test',         // VVV
460
			'local.wordpress-trunk.test',   // VVV
461
			'src.wordpress-develop.test',   // VVV
462
			'build.wordpress-develop.test', // VVV
463
		);
464
		if ( in_array( $domain, $forbidden_domains ) ) {
465
			return new WP_Error( 'fail_domain_forbidden', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', 'jetpack' ), $domain ) );
466
		}
467
468
		// No .test or .local domains
469 View Code Duplication
		if ( preg_match( '#\.(test|local)$#i', $domain ) ) {
470
			return new WP_Error( 'fail_domain_tld', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', 'jetpack' ), $domain ) );
471
		}
472
473
		// No WPCOM subdomains
474 View Code Duplication
		if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) {
475
			return new WP_Error( 'fail_subdomain_wpcom', sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', 'jetpack' ), $domain ) );
476
		}
477
478
		// If PHP was compiled without support for the Filter module (very edge case)
479
		if ( ! function_exists( 'filter_var' ) ) {
480
			// Just pass back true for now, and let wpcom sort it out.
481
			return true;
482
		}
483
484
		return true;
485
	}
486
}
487