Completed
Push — add/magic-link-for-mobile ( a67c5b...4ebb52 )
by
unknown
13:24 queued 06:01
created

Manager::verify_secrets()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 116

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 10
nop 3
dl 0
loc 116
rs 5.5733
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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