Completed
Push — try/namespacing-all-the-things ( 457764 )
by
unknown
08:24
created

Manager::delete_secrets()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 11
rs 9.9
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\Constants;
11
use Automattic\Jetpack\Tracking;
12
use Automattic\Jetpack\Jetpack_Options;
13
14
/**
15
 * The Jetpack Connection Manager class that is used as a single gateway between WordPress.com
16
 * and Jetpack.
17
 */
18
class Manager implements Manager_Interface {
19
20
	const SECRETS_MISSING        = 'secrets_missing';
21
	const SECRETS_EXPIRED        = 'secrets_expired';
22
	const SECRETS_OPTION_NAME    = 'jetpack_secrets';
23
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
24
	const JETPACK_MASTER_USER    = true;
25
26
	/**
27
	 * The procedure that should be run to generate secrets.
28
	 *
29
	 * @var Callable
30
	 */
31
	protected $secret_callable;
32
33
	/**
34
	 * Initializes all needed hooks and request handlers. Handles API calls, upload
35
	 * requests, authentication requests. Also XMLRPC options requests.
36
	 * Fallback XMLRPC is also a bridge, but probably can be a class that inherits
37
	 * this one. Among other things it should strip existing methods.
38
	 *
39
	 * @param Array $methods an array of API method names for the Connection to accept and
40
	 *                       pass on to existing callables. It's possible to specify whether
41
	 *                       each method should be available for unauthenticated calls or not.
42
	 * @see Jetpack::__construct
43
	 */
44
	public function initialize( $methods ) {
45
		$methods;
46
	}
47
48
	/**
49
	 * Returns true if the current site is connected to WordPress.com.
50
	 *
51
	 * @return Boolean is the site connected?
52
	 */
53
	public function is_active() {
54
		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...
55
	}
56
57
	/**
58
	 * Returns true if the user with the specified identifier is connected to
59
	 * WordPress.com.
60
	 *
61
	 * @param Integer $user_id the user identifier.
62
	 * @return Boolean is the user connected?
63
	 */
64
	public function is_user_connected( $user_id ) {
65
		return $user_id;
66
	}
67
68
	/**
69
	 * Get the wpcom user data of the current|specified connected user.
70
	 *
71
	 * @param Integer $user_id the user identifier.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $user_id not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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