Completed
Push — add/is-active-to-connection-pa... ( 30e22c )
by
unknown
18:06 queued 10:36
created

Manager::delete_secrets()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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