Completed
Push — update/gardening-action-extrac... ( 967522...8b9cf8 )
by Jeremy
24:34 queued 12:22
created

Tokens::validate_blog_token()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 24

Duplication

Lines 3
Ratio 12.5 %

Importance

Changes 0
Metric Value
cc 7
nc 5
nop 0
dl 3
loc 24
rs 8.6026
c 0
b 0
f 0
1
<?php
2
/**
3
 * The Jetpack Connection Tokens class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
use Automattic\Jetpack\Constants;
11
use Automattic\Jetpack\Roles;
12
use Jetpack_Options;
13
use WP_Error;
14
15
/**
16
 * The Jetpack Connection Tokens class that manages tokens.
17
 */
18
class Tokens {
19
20
	const MAGIC_NORMAL_TOKEN_KEY = ';normal;';
21
22
	/**
23
	 * Deletes all connection tokens and transients from the local Jetpack site.
24
	 */
25
	public function delete_all() {
26
		Jetpack_Options::delete_option(
27
			array(
28
				'blog_token',
29
				'user_token',
30
				'user_tokens',
31
			)
32
		);
33
	}
34
35
	/**
36
	 * Perform the API request to validate the blog and user tokens.
37
	 *
38
	 * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
39
	 *
40
	 * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
41
	 */
42
	public function validate( $user_id = null ) {
43
		$blog_id = Jetpack_Options::get_option( 'id' );
44
		if ( ! $blog_id ) {
45
			return new WP_Error( 'site_not_registered', 'Site not registered.' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'site_not_registered'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
46
		}
47
		$url = sprintf(
48
			'%s/%s/v%s/%s',
49
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
50
			'wpcom',
51
			'2',
52
			'sites/' . $blog_id . '/jetpack-token-health'
53
		);
54
55
		$user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() );
56
		$blog_token = $this->get_access_token();
57
		$method     = 'POST';
58
		$body       = array(
59
			'user_token' => $this->get_signed_token( $user_token ),
0 ignored issues
show
Security Bug introduced by
It seems like $user_token defined by $this->get_access_token(... get_current_user_id()) on line 55 can also be of type false; however, Automattic\Jetpack\Conne...ens::get_signed_token() does only seem to accept object, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
60
			'blog_token' => $this->get_signed_token( $blog_token ),
0 ignored issues
show
Security Bug introduced by
It seems like $blog_token defined by $this->get_access_token() on line 56 can also be of type false; however, Automattic\Jetpack\Conne...ens::get_signed_token() does only seem to accept object, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
61
		);
62
		$response   = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
63
64 View Code Duplication
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
65
			return false;
66
		}
67
68
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
69
70
		return $body ? $body : false;
71
	}
72
73
	/**
74
	 * Perform the API request to validate only the blog.
75
	 *
76
	 * @return bool|WP_Error Boolean with the test result. WP_Error if test cannot be performed.
77
	 */
78
	public function validate_blog_token() {
79
		$blog_id = Jetpack_Options::get_option( 'id' );
80
		if ( ! $blog_id ) {
81
			return new WP_Error( 'site_not_registered', 'Site not registered.' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'site_not_registered'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
82
		}
83
		$url = sprintf(
84
			'%s/%s/v%s/%s',
85
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
86
			'wpcom',
87
			'2',
88
			'sites/' . $blog_id . '/jetpack-token-health/blog'
89
		);
90
91
		$method   = 'GET';
92
		$response = Client::remote_request( compact( 'url', 'method' ) );
93
94 View Code Duplication
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
95
			return false;
96
		}
97
98
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
99
100
		return is_array( $body ) && isset( $body['is_healthy'] ) && true === $body['is_healthy'];
101
	}
102
103
	/**
104
	 * Obtains the auth token.
105
	 *
106
	 * @param array  $data The request data.
107
	 * @param string $token_api_url The URL of the Jetpack "token" API.
108
	 * @return object|WP_Error Returns the auth token on success.
109
	 *                          Returns a WP_Error on failure.
110
	 */
111
	public function get( $data, $token_api_url ) {
112
		$roles = new Roles();
113
		$role  = $roles->translate_current_user_to_role();
114
115
		if ( ! $role ) {
116
			return new WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'role'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
117
		}
118
119
		$client_secret = $this->get_access_token();
120
		if ( ! $client_secret ) {
121
			return new WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'client_secret'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
122
		}
123
124
		/**
125
		 * Filter the URL of the first time the user gets redirected back to your site for connection
126
		 * data processing.
127
		 *
128
		 * @since 8.0.0
129
		 *
130
		 * @param string $redirect_url Defaults to the site admin URL.
131
		 */
132
		$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
133
134
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
135
136
		/**
137
		* Filter the URL to redirect the user back to when the authentication process
138
		* is complete.
139
		*
140
		* @since 8.0.0
141
		*
142
		* @param string $redirect_url Defaults to the site URL.
143
		*/
144
		$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
145
146
		$redirect_uri = ( 'calypso' === $data['auth_type'] )
147
			? $data['redirect_uri']
148
			: add_query_arg(
149
				array(
150
					'handler'  => 'jetpack-connection-webhooks',
151
					'action'   => 'authorize',
152
					'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
153
					'redirect' => $redirect ? rawurlencode( $redirect ) : false,
154
				),
155
				esc_url( $processing_url )
156
			);
157
158
		/**
159
		 * Filters the token request data.
160
		 *
161
		 * @since 8.0.0
162
		 *
163
		 * @param array $request_data request data.
164
		 */
165
		$body = apply_filters(
166
			'jetpack_token_request_body',
167
			array(
168
				'client_id'     => Jetpack_Options::get_option( 'id' ),
169
				'client_secret' => $client_secret->secret,
170
				'grant_type'    => 'authorization_code',
171
				'code'          => $data['code'],
172
				'redirect_uri'  => $redirect_uri,
173
			)
174
		);
175
176
		$args = array(
177
			'method'  => 'POST',
178
			'body'    => $body,
179
			'headers' => array(
180
				'Accept' => 'application/json',
181
			),
182
		);
183
		add_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
184
		$response = Client::_wp_remote_request( $token_api_url, $args );
185
		remove_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
186
187
		if ( is_wp_error( $response ) ) {
188
			return new WP_Error( 'token_http_request_failed', $response->get_error_message() );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'token_http_request_failed'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
189
		}
190
191
		$code   = wp_remote_retrieve_response_code( $response );
192
		$entity = wp_remote_retrieve_body( $response );
193
194
		if ( $entity ) {
195
			$json = json_decode( $entity );
196
		} else {
197
			$json = false;
198
		}
199
200 View Code Duplication
		if ( 200 !== $code || ! empty( $json->error ) ) {
201
			if ( empty( $json->error ) ) {
202
				return new WP_Error( 'unknown', '', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
203
			}
204
205
			/* translators: Error description string. */
206
			$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : '';
207
208
			return new WP_Error( (string) $json->error, $error_description, $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with (string) $json->error.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
209
		}
210
211
		if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
212
			return new WP_Error( 'access_token', '', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'access_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
213
		}
214
215
		if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
216
			return new WP_Error( 'token_type', '', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'token_type'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
217
		}
218
219
		if ( empty( $json->scope ) ) {
220
			return new WP_Error( 'scope', 'No Scope', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'scope'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
221
		}
222
223
		// TODO: get rid of the error silencer.
224
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
225
		@list( $role, $hmac ) = explode( ':', $json->scope );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
226
		if ( empty( $role ) || empty( $hmac ) ) {
227
			return new WP_Error( 'scope', 'Malformed Scope', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'scope'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228
		}
229
230
		if ( $this->sign_role( $role ) !== $json->scope ) {
231
			return new WP_Error( 'scope', 'Invalid Scope', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'scope'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
232
		}
233
234
		$cap = $roles->translate_role_to_cap( $role );
235
		if ( ! $cap ) {
236
			return new WP_Error( 'scope', 'No Cap', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'scope'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
237
		}
238
239
		if ( ! current_user_can( $cap ) ) {
240
			return new WP_Error( 'scope', 'current_user_cannot', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'scope'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
241
		}
242
243
		return (string) $json->access_token;
244
	}
245
246
	/**
247
	 * Enters a user token into the user_tokens option
248
	 *
249
	 * @param int    $user_id The user id.
250
	 * @param string $token The user token.
251
	 * @param bool   $is_master_user Whether the user is the master user.
252
	 * @return bool
253
	 */
254
	public function update_user_token( $user_id, $token, $is_master_user ) {
255
		// Not designed for concurrent updates.
256
		$user_tokens = Jetpack_Options::get_option( 'user_tokens' );
257
		if ( ! is_array( $user_tokens ) ) {
258
			$user_tokens = array();
259
		}
260
		$user_tokens[ $user_id ] = $token;
261
		if ( $is_master_user ) {
262
			$master_user = $user_id;
263
			$options     = compact( 'user_tokens', 'master_user' );
264
		} else {
265
			$options = compact( 'user_tokens' );
266
		}
267
		return Jetpack_Options::update_options( $options );
268
	}
269
270
	/**
271
	 * Sign a user role with the master access token.
272
	 * If not specified, will default to the current user.
273
	 *
274
	 * @access public
275
	 *
276
	 * @param string $role    User role.
277
	 * @param int    $user_id ID of the user.
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...
278
	 * @return string Signed user role.
279
	 */
280
	public function sign_role( $role, $user_id = null ) {
281
		if ( empty( $user_id ) ) {
282
			$user_id = (int) get_current_user_id();
283
		}
284
285
		if ( ! $user_id ) {
286
			return false;
287
		}
288
289
		$token = $this->get_access_token();
290
		if ( ! $token || is_wp_error( $token ) ) {
291
			return false;
292
		}
293
294
		return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
295
	}
296
297
	/**
298
	 * Increases the request timeout value to 30 seconds.
299
	 *
300
	 * @return int Returns 30.
301
	 */
302
	public function return_30() {
303
		return 30;
304
	}
305
306
	/**
307
	 * Gets the requested token.
308
	 *
309
	 * Tokens are one of two types:
310
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
311
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
312
	 *    are not associated with a user account. They represent the site's connection with
313
	 *    the Jetpack servers.
314
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
315
	 *
316
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
317
	 * token, and $private is a secret that should never be displayed anywhere or sent
318
	 * over the network; it's used only for signing things.
319
	 *
320
	 * Blog Tokens can be "Normal" or "Special".
321
	 * * Normal: The result of a normal connection flow. They look like
322
	 *   "{$random_string_1}.{$random_string_2}"
323
	 *   That is, $token_key and $private are both random strings.
324
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
325
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
326
	 *   constant (rare).
327
	 * * Special: A connection token for sites that have gone through an alternative
328
	 *   connection flow. They look like:
329
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
330
	 *   That is, $private is a random string and $token_key has a special structure with
331
	 *   lots of semicolons.
332
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
333
	 *   JETPACK_BLOG_TOKEN constant.
334
	 *
335
	 * In particular, note that Normal Blog Tokens never start with ";" and that
336
	 * Special Blog Tokens always do.
337
	 *
338
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
339
	 * order:
340
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
341
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
342
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
343
	 *
344
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
345
	 * @param string|false $token_key If provided, check that the token matches the provided input.
346
	 * @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.
347
	 *
348
	 * @return object|false
349
	 */
350
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
351
		$possible_special_tokens = array();
352
		$possible_normal_tokens  = array();
353
		$user_tokens             = Jetpack_Options::get_option( 'user_tokens' );
354
355
		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...
356
			if ( ! $user_tokens ) {
357
				return $suppress_errors ? false : new WP_Error( 'no_user_tokens', __( 'No user tokens found', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_user_tokens'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
358
			}
359
			if ( true === $user_id ) { // connection owner.
360
				$user_id = Jetpack_Options::get_option( 'master_user' );
361
				if ( ! $user_id ) {
362
					return $suppress_errors ? false : new WP_Error( 'empty_master_user_option', __( 'No primary user defined', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'empty_master_user_option'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
363
				}
364
			}
365
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
366
				// translators: %s is the user ID.
367
				return $suppress_errors ? false : new WP_Error( 'no_token_for_user', sprintf( __( 'No token for user %d', 'jetpack' ), $user_id ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_token_for_user'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
368
			}
369
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
370
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
371
				// translators: %s is the user ID.
372
				return $suppress_errors ? false : new WP_Error( 'token_malformed', sprintf( __( 'Token for user %d is malformed', 'jetpack' ), $user_id ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'token_malformed'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
373
			}
374
			if ( $user_token_chunks[2] !== (string) $user_id ) {
375
				// translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
376
				return $suppress_errors ? false : new WP_Error( 'user_id_mismatch', sprintf( __( 'Requesting user_id %1$d does not match token user_id %2$d', 'jetpack' ), $user_id, $user_token_chunks[2] ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'user_id_mismatch'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
377
			}
378
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
379
		} else {
380
			$stored_blog_token = Jetpack_Options::get_option( 'blog_token' );
381
			if ( $stored_blog_token ) {
382
				$possible_normal_tokens[] = $stored_blog_token;
383
			}
384
385
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
386
387
			if ( $defined_tokens_string ) {
388
				$defined_tokens = explode( ',', $defined_tokens_string );
389
				foreach ( $defined_tokens as $defined_token ) {
390
					if ( ';' === $defined_token[0] ) {
391
						$possible_special_tokens[] = $defined_token;
392
					} else {
393
						$possible_normal_tokens[] = $defined_token;
394
					}
395
				}
396
			}
397
		}
398
399
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
400
			$possible_tokens = $possible_normal_tokens;
401
		} else {
402
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
403
		}
404
405
		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...
406
			// If no user tokens were found, it would have failed earlier, so this is about blog token.
407
			return $suppress_errors ? false : new WP_Error( 'no_possible_tokens', __( 'No blog token found', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_possible_tokens'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
408
		}
409
410
		$valid_token = false;
411
412
		if ( false === $token_key ) {
413
			// Use first token.
414
			$valid_token = $possible_tokens[0];
415
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
416
			// Use first normal token.
417
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
418
		} else {
419
			// Use the token matching $token_key or false if none.
420
			// Ensure we check the full key.
421
			$token_check = rtrim( $token_key, '.' ) . '.';
422
423
			foreach ( $possible_tokens as $possible_token ) {
424
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
425
					$valid_token = $possible_token;
426
					break;
427
				}
428
			}
429
		}
430
431
		if ( ! $valid_token ) {
432
			if ( $user_id ) {
433
				// translators: %d is the user ID.
434
				return $suppress_errors ? false : new WP_Error( 'no_valid_user_token', sprintf( __( 'Invalid token for user %d', 'jetpack' ), $user_id ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_valid_user_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
435
			} else {
436
				return $suppress_errors ? false : new WP_Error( 'no_valid_blog_token', __( 'Invalid blog token', 'jetpack' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'no_valid_blog_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
437
			}
438
		}
439
440
		return (object) array(
441
			'secret'           => $valid_token,
442
			'external_user_id' => (int) $user_id,
443
		);
444
	}
445
446
	/**
447
	 * Updates the blog token to a new value.
448
	 *
449
	 * @access public
450
	 *
451
	 * @param string $token the new blog token value.
452
	 * @return Boolean Whether updating the blog token was successful.
453
	 */
454
	public function update_blog_token( $token ) {
455
		return Jetpack_Options::update_option( 'blog_token', $token );
456
	}
457
458
	/**
459
	 * Unlinks the current user from the linked WordPress.com user.
460
	 *
461
	 * @access public
462
	 * @static
463
	 *
464
	 * @todo Refactor to properly load the XMLRPC client independently.
465
	 *
466
	 * @param Integer $user_id the user identifier.
467
	 * @param bool    $can_overwrite_primary_user Allow for the primary user to be disconnected.
468
	 * @return Boolean Whether the disconnection of the user was successful.
469
	 */
470
	public function disconnect_user( $user_id, $can_overwrite_primary_user = false ) {
471
		$tokens = Jetpack_Options::get_option( 'user_tokens' );
472
		if ( ! $tokens ) {
473
			return false;
474
		}
475
476
		if ( Jetpack_Options::get_option( 'master_user' ) === $user_id && ! $can_overwrite_primary_user ) {
477
			return false;
478
		}
479
480
		if ( ! isset( $tokens[ $user_id ] ) ) {
481
			return false;
482
		}
483
484
		unset( $tokens[ $user_id ] );
485
486
		Jetpack_Options::update_option( 'user_tokens', $tokens );
487
488
		return true;
489
	}
490
491
	/**
492
	 * Returns an array of user_id's that have user tokens for communicating with wpcom.
493
	 * Able to select by specific capability.
494
	 *
495
	 * @param string $capability The capability of the user.
496
	 * @return array Array of WP_User objects if found.
497
	 */
498
	public function get_connected_users( $capability = 'any' ) {
499
		$connected_users = array();
500
		$user_tokens     = Jetpack_Options::get_option( 'user_tokens' );
501
502
		if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) {
503
			return $connected_users;
504
		}
505
		$connected_user_ids = array_keys( $user_tokens );
506
507
		if ( ! empty( $connected_user_ids ) ) {
508
			foreach ( $connected_user_ids as $id ) {
509
				// Check for capability.
510
				if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
511
					continue;
512
				}
513
514
				$user_data = get_userdata( $id );
515
				if ( $user_data instanceof \WP_User ) {
0 ignored issues
show
Bug introduced by
The class WP_User does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
516
					$connected_users[] = $user_data;
517
				}
518
			}
519
		}
520
521
		return $connected_users;
522
	}
523
524
	/**
525
	 * Fetches a signed token.
526
	 *
527
	 * @param object $token the token.
528
	 * @return WP_Error|string a signed token
529
	 */
530
	public function get_signed_token( $token ) {
531
		if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
532
			return new WP_Error( 'invalid_token' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
533
		}
534
535
		list( $token_key, $token_secret ) = explode( '.', $token->secret );
536
537
		$token_key = sprintf(
538
			'%s:%d:%d',
539
			$token_key,
540
			Constants::get_constant( 'JETPACK__API_VERSION' ),
541
			$token->external_user_id
542
		);
543
544
		$timestamp = time();
545
546 View Code Duplication
		if ( function_exists( 'wp_generate_password' ) ) {
547
			$nonce = wp_generate_password( 10, false );
548
		} else {
549
			$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
550
		}
551
552
		$normalized_request_string = join(
553
			"\n",
554
			array(
555
				$token_key,
556
				$timestamp,
557
				$nonce,
558
			)
559
		) . "\n";
560
561
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
562
		$signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
563
564
		$auth = array(
565
			'token'     => $token_key,
566
			'timestamp' => $timestamp,
567
			'nonce'     => $nonce,
568
			'signature' => $signature,
569
		);
570
571
		$header_pieces = array();
572
		foreach ( $auth as $key => $value ) {
573
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
574
		}
575
576
		return join( ' ', $header_pieces );
577
	}
578
}
579