Completed
Push — try/refactor-secrets-and-token... ( 313ee5 )
by
unknown
381:48 queued 371:37
created

Tokens::update_user_token()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 3
dl 0
loc 15
rs 9.7666
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 Automattic\Jetpack\Tracking;
13
use Jetpack_Options;
14
use WP_Error;
15
16
/**
17
 * The Jetpack Connection Tokens class that manages tokens.
18
 */
19
class Tokens {
20
21
	const SECRETS_MISSING            = 'secrets_missing';
22
	const SECRETS_EXPIRED            = 'secrets_expired';
23
	const LEGACY_SECRETS_OPTION_NAME = 'jetpack_secrets';
24
	const MAGIC_NORMAL_TOKEN_KEY     = ';normal;';
25
26
	/**
27
	 * Constant used to fetch the master user token. Deprecated.
28
	 *
29
	 * @deprecated 9.0.0
30
	 * @see Manager::CONNECTION_OWNER
31
	 * @var boolean
32
	 */
33
	const JETPACK_MASTER_USER = true; //phpcs:ignore Jetpack.Constants.MasterUserConstant.ShouldNotBeUsed
34
35
	/**
36
	 * For internal use only. If you need to get the connection owner, use the provided methods
37
	 * get_connection_owner_id, get_connection_owner and is_connection_owner
38
	 *
39
	 * @todo Add private visibility once PHP 7.1 is the minimum supported verion.
40
	 *
41
	 * @var boolean
42
	 */
43
	const CONNECTION_OWNER = true;
44
45
	/**
46
	 * Deletes all connection tokens and transients from the local Jetpack site.
47
	 *
48
	 * @return bool True if disconnected successfully, false otherwise.
49
	 */
50
	public static function delete_all() {
51
		/**
52
		 * Fires upon the disconnect attempt.
53
		 * Return `false` to prevent the disconnect.
54
		 *
55
		 * @since 8.7.0
56
		 */
57
		if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) {
58
			return false;
59
		}
60
61
		\Jetpack_Options::delete_option(
62
			array(
63
				'blog_token',
64
				'user_token',
65
				'user_tokens',
66
				'master_user',
67
				'time_diff',
68
				'fallback_no_verify_ssl_certs',
69
			)
70
		);
71
72
		\Jetpack_Options::delete_raw_option( 'jetpack_secrets' );
73
74
		// Delete cached connected user data.
75
		$transient_key = 'jetpack_connected_user_data_' . get_current_user_id();
76
		delete_transient( $transient_key );
77
78
		// Delete all XML-RPC errors.
79
		Error_Handler::get_instance()->delete_all_errors();
80
81
		return true;
82
	}
83
84
	/**
85
	 * Perform the API request to validate the blog and user tokens.
86
	 *
87
	 * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
88
	 *
89
	 * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
90
	 */
91
	public static function validate( $user_id = null ) {
92
		$blog_id = Jetpack_Options::get_option( 'id' );
93
		if ( ! $blog_id ) {
94
			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...
95
		}
96
		$url = sprintf(
97
			'%s/%s/v%s/%s',
98
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
99
			'wpcom',
100
			'2',
101
			'sites/' . $blog_id . '/jetpack-token-health'
102
		);
103
104
		$user_token = self::get_access_token( $user_id ? $user_id : get_current_user_id() );
105
		$blog_token = self::get_access_token();
106
		$method     = 'POST';
107
		$body       = array(
108
			'user_token' => self::get_signed_token( $user_token ),
0 ignored issues
show
Security Bug introduced by
It seems like $user_token defined by self::get_access_token($... get_current_user_id()) on line 104 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...
109
			'blog_token' => self::get_signed_token( $blog_token ),
0 ignored issues
show
Security Bug introduced by
It seems like $blog_token defined by self::get_access_token() on line 105 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...
110
		);
111
		$response   = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
112
113
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
114
			return false;
115
		}
116
117
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
118
119
		return $body ? $body : false;
120
	}
121
122
	/**
123
	 * Obtains the auth token.
124
	 *
125
	 * @param array  $data The request data.
126
	 * @param string $token_api_url The URL of the Jetpack "token" API.
127
	 * @return object|\WP_Error Returns the auth token on success.
128
	 *                          Returns a \WP_Error on failure.
129
	 */
130
	public static function get( $data, $token_api_url ) {
131
		$roles = new Roles();
132
		$role  = $roles->translate_current_user_to_role();
133
134
		if ( ! $role ) {
135
			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...
136
		}
137
138
		$client_secret = self::get_access_token();
139
		if ( ! $client_secret ) {
140
			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...
141
		}
142
143
		/**
144
		 * Filter the URL of the first time the user gets redirected back to your site for connection
145
		 * data processing.
146
		 *
147
		 * @since 8.0.0
148
		 *
149
		 * @param string $redirect_url Defaults to the site admin URL.
150
		 */
151
		$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
152
153
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
154
155
		/**
156
		* Filter the URL to redirect the user back to when the authentication process
157
		* is complete.
158
		*
159
		* @since 8.0.0
160
		*
161
		* @param string $redirect_url Defaults to the site URL.
162
		*/
163
		$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
164
165
		$redirect_uri = ( 'calypso' === $data['auth_type'] )
166
			? $data['redirect_uri']
167
			: add_query_arg(
168
				array(
169
					'handler'  => 'jetpack-connection-webhooks',
170
					'action'   => 'authorize',
171
					'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
172
					'redirect' => $redirect ? rawurlencode( $redirect ) : false,
173
				),
174
				esc_url( $processing_url )
175
			);
176
177
		/**
178
		 * Filters the token request data.
179
		 *
180
		 * @since 8.0.0
181
		 *
182
		 * @param array $request_data request data.
183
		 */
184
		$body = apply_filters(
185
			'jetpack_token_request_body',
186
			array(
187
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
188
				'client_secret' => $client_secret->secret,
189
				'grant_type'    => 'authorization_code',
190
				'code'          => $data['code'],
191
				'redirect_uri'  => $redirect_uri,
192
			)
193
		);
194
195
		$args = array(
196
			'method'  => 'POST',
197
			'body'    => $body,
198
			'headers' => array(
199
				'Accept' => 'application/json',
200
			),
201
		);
202
		add_filter( 'http_request_timeout', array( get_called_class(), 'return_30' ), PHP_INT_MAX - 1 );
203
		$response = Client::_wp_remote_request( $token_api_url, $args );
204
		remove_filter( 'http_request_timeout', array( get_called_class(), 'return_30' ), PHP_INT_MAX - 1 );
205
206
		if ( is_wp_error( $response ) ) {
207
			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...
208
		}
209
210
		$code   = wp_remote_retrieve_response_code( $response );
211
		$entity = wp_remote_retrieve_body( $response );
212
213
		if ( $entity ) {
214
			$json = json_decode( $entity );
215
		} else {
216
			$json = false;
217
		}
218
219 View Code Duplication
		if ( 200 !== $code || ! empty( $json->error ) ) {
220
			if ( empty( $json->error ) ) {
221
				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...
222
			}
223
224
			/* translators: Error description string. */
225
			$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : '';
226
227
			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...
228
		}
229
230
		if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
231
			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...
232
		}
233
234
		if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
235
			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...
236
		}
237
238
		if ( empty( $json->scope ) ) {
239
			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...
240
		}
241
242
		// TODO: get rid of the error silencer.
243
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
244
		@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...
245
		if ( empty( $role ) || empty( $hmac ) ) {
246
			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...
247
		}
248
249
		if ( self::sign_role( $role ) !== $json->scope ) {
250
			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...
251
		}
252
253
		$cap = $roles->translate_role_to_cap( $role );
254
		if ( ! $cap ) {
255
			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...
256
		}
257
258
		if ( ! current_user_can( $cap ) ) {
259
			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...
260
		}
261
262
		return (string) $json->access_token;
263
	}
264
265
	/**
266
	 * Enters a user token into the user_tokens option
267
	 *
268
	 * @param int    $user_id The user id.
269
	 * @param string $token The user token.
270
	 * @param bool   $is_master_user Whether the user is the master user.
271
	 * @return bool
272
	 */
273
	public static function update_user_token( $user_id, $token, $is_master_user ) {
274
		// Not designed for concurrent updates.
275
		$user_tokens = \Jetpack_Options::get_option( 'user_tokens' );
276
		if ( ! is_array( $user_tokens ) ) {
277
			$user_tokens = array();
278
		}
279
		$user_tokens[ $user_id ] = $token;
280
		if ( $is_master_user ) {
281
			$master_user = $user_id;
282
			$options     = compact( 'user_tokens', 'master_user' );
283
		} else {
284
			$options = compact( 'user_tokens' );
285
		}
286
		return \Jetpack_Options::update_options( $options );
287
	}
288
289
	/**
290
	 * Sign a user role with the master access token.
291
	 * If not specified, will default to the current user.
292
	 *
293
	 * @access public
294
	 *
295
	 * @param string $role    User role.
296
	 * @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...
297
	 * @return string Signed user role.
298
	 */
299
	public static function sign_role( $role, $user_id = null ) {
300
		if ( empty( $user_id ) ) {
301
			$user_id = (int) get_current_user_id();
302
		}
303
304
		if ( ! $user_id ) {
305
			return false;
306
		}
307
308
		$token = self::get_access_token();
309
		if ( ! $token || is_wp_error( $token ) ) {
310
			return false;
311
		}
312
313
		return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
314
	}
315
316
	/**
317
	 * Increases the request timeout value to 30 seconds.
318
	 *
319
	 * @return int Returns 30.
320
	 */
321
	public static function return_30() {
322
		return 30;
323
	}
324
325
	/**
326
	 * Gets the requested token.
327
	 *
328
	 * Tokens are one of two types:
329
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
330
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
331
	 *    are not associated with a user account. They represent the site's connection with
332
	 *    the Jetpack servers.
333
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
334
	 *
335
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
336
	 * token, and $private is a secret that should never be displayed anywhere or sent
337
	 * over the network; it's used only for signing things.
338
	 *
339
	 * Blog Tokens can be "Normal" or "Special".
340
	 * * Normal: The result of a normal connection flow. They look like
341
	 *   "{$random_string_1}.{$random_string_2}"
342
	 *   That is, $token_key and $private are both random strings.
343
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
344
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
345
	 *   constant (rare).
346
	 * * Special: A connection token for sites that have gone through an alternative
347
	 *   connection flow. They look like:
348
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
349
	 *   That is, $private is a random string and $token_key has a special structure with
350
	 *   lots of semicolons.
351
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
352
	 *   JETPACK_BLOG_TOKEN constant.
353
	 *
354
	 * In particular, note that Normal Blog Tokens never start with ";" and that
355
	 * Special Blog Tokens always do.
356
	 *
357
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
358
	 * order:
359
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
360
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
361
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
362
	 *
363
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
364
	 * @param string|false $token_key If provided, check that the token matches the provided input.
365
	 * @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.
366
	 *
367
	 * @return object|false
368
	 */
369
	public static function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
370
		$possible_special_tokens = array();
371
		$possible_normal_tokens  = array();
372
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
373
374
		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...
375
			if ( ! $user_tokens ) {
376
				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...
377
			}
378
			if ( self::CONNECTION_OWNER === $user_id ) {
379
				$user_id = \Jetpack_Options::get_option( 'master_user' );
380
				if ( ! $user_id ) {
381
					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...
382
				}
383
			}
384
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
385
				// translators: %s is the user ID.
386
				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...
387
			}
388
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
389 View Code Duplication
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
390
				// translators: %s is the user ID.
391
				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...
392
			}
393
			if ( $user_token_chunks[2] !== (string) $user_id ) {
394
				// translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
395
				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...
396
			}
397
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
398
		} else {
399
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
400
			if ( $stored_blog_token ) {
401
				$possible_normal_tokens[] = $stored_blog_token;
402
			}
403
404
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
405
406
			if ( $defined_tokens_string ) {
407
				$defined_tokens = explode( ',', $defined_tokens_string );
408
				foreach ( $defined_tokens as $defined_token ) {
409
					if ( ';' === $defined_token[0] ) {
410
						$possible_special_tokens[] = $defined_token;
411
					} else {
412
						$possible_normal_tokens[] = $defined_token;
413
					}
414
				}
415
			}
416
		}
417
418
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
419
			$possible_tokens = $possible_normal_tokens;
420
		} else {
421
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
422
		}
423
424
		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...
425
			// If no user tokens were found, it would have failed earlier, so this is about blog token.
426
			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...
427
		}
428
429
		$valid_token = false;
430
431
		if ( false === $token_key ) {
432
			// Use first token.
433
			$valid_token = $possible_tokens[0];
434
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
435
			// Use first normal token.
436
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
437
		} else {
438
			// Use the token matching $token_key or false if none.
439
			// Ensure we check the full key.
440
			$token_check = rtrim( $token_key, '.' ) . '.';
441
442
			foreach ( $possible_tokens as $possible_token ) {
443
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
444
					$valid_token = $possible_token;
445
					break;
446
				}
447
			}
448
		}
449
450
		if ( ! $valid_token ) {
451
			if ( $user_id ) {
452
				// translators: %d is the user ID.
453
				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...
454
			} else {
455
				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...
456
			}
457
		}
458
459
		return (object) array(
460
			'secret'           => $valid_token,
461
			'external_user_id' => (int) $user_id,
462
		);
463
	}
464
465
	/**
466
	 * Perform the API request to refresh the blog token.
467
	 * Note that we are making this request on behalf of the Jetpack master user,
468
	 * given they were (most probably) the ones that registered the site at the first place.
469
	 *
470
	 * @return WP_Error|bool The result of updating the blog_token option.
471
	 */
472
	public static function refresh_blog_token() {
473
		( new Tracking() )->record_user_event( 'restore_connection_refresh_blog_token' );
474
475
		$blog_id = Jetpack_Options::get_option( 'id' );
476
		if ( ! $blog_id ) {
477
			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...
478
		}
479
480
		$url     = sprintf(
481
			'%s/%s/v%s/%s',
482
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
483
			'wpcom',
484
			'2',
485
			'sites/' . $blog_id . '/jetpack-refresh-blog-token'
486
		);
487
		$method  = 'POST';
488
		$user_id = get_current_user_id();
489
490
		$response = Client::remote_request( compact( 'url', 'method', 'user_id' ) );
491
492
		if ( is_wp_error( $response ) ) {
493
			return new WP_Error( 'refresh_blog_token_http_request_failed', $response->get_error_message() );
0 ignored issues
show
Bug introduced by
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'refresh_blog_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...
494
		}
495
496
		$code   = wp_remote_retrieve_response_code( $response );
497
		$entity = wp_remote_retrieve_body( $response );
498
499
		if ( $entity ) {
500
			$json = json_decode( $entity );
501
		} else {
502
			$json = false;
503
		}
504
505 View Code Duplication
		if ( 200 !== $code ) {
506
			if ( empty( $json->code ) ) {
507
				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...
508
			}
509
510
			/* translators: Error description string. */
511
			$error_description = isset( $json->message ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->message ) : '';
512
513
			return new WP_Error( (string) $json->code, $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->code.

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...
514
		}
515
516
		if ( empty( $json->jetpack_secret ) || ! is_scalar( $json->jetpack_secret ) ) {
517
			return new WP_Error( 'jetpack_secret', '', $code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'jetpack_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...
518
		}
519
520
		return Jetpack_Options::update_option( 'blog_token', (string) $json->jetpack_secret );
521
	}
522
523
	/**
524
	 * Disconnect the user from WP.com, and initiate the reconnect process.
525
	 *
526
	 * @return bool
527
	 */
528
	public static function refresh_user_token() {
529
		( new Tracking() )->record_user_event( 'restore_connection_refresh_user_token' );
530
531
		self::disconnect_user( null, true );
532
533
		return true;
534
	}
535
536
	/**
537
	 * Unlinks the current user from the linked WordPress.com user.
538
	 *
539
	 * @access public
540
	 * @static
541
	 *
542
	 * @todo Refactor to properly load the XMLRPC client independently.
543
	 *
544
	 * @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...
545
	 * @param bool    $can_overwrite_primary_user Allow for the primary user to be disconnected.
546
	 * @return Boolean Whether the disconnection of the user was successful.
547
	 */
548
	public static function disconnect_user( $user_id = null, $can_overwrite_primary_user = false ) {
549
		$tokens = Jetpack_Options::get_option( 'user_tokens' );
550
		if ( ! $tokens ) {
551
			return false;
552
		}
553
554
		$user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id;
555
556
		if ( Jetpack_Options::get_option( 'master_user' ) === $user_id && ! $can_overwrite_primary_user ) {
557
			return false;
558
		}
559
560
		if ( ! isset( $tokens[ $user_id ] ) ) {
561
			return false;
562
		}
563
564
		$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) );
565
		$xml->query( 'jetpack.unlink_user', $user_id );
566
567
		unset( $tokens[ $user_id ] );
568
569
		Jetpack_Options::update_option( 'user_tokens', $tokens );
570
571
		// Delete cached connected user data.
572
		$transient_key = "jetpack_connected_user_data_$user_id";
573
		delete_transient( $transient_key );
574
575
		/**
576
		 * Fires after the current user has been unlinked from WordPress.com.
577
		 *
578
		 * @since 4.1.0
579
		 *
580
		 * @param int $user_id The current user's ID.
581
		 */
582
		do_action( 'jetpack_unlinked_user', $user_id );
583
584
		return true;
585
	}
586
587
	/**
588
	 * Returns an array of user_id's that have user tokens for communicating with wpcom.
589
	 * Able to select by specific capability.
590
	 *
591
	 * @param string $capability The capability of the user.
592
	 * @return array Array of WP_User objects if found.
593
	 */
594
	public static function get_connected_users( $capability = 'any' ) {
595
		$connected_users = array();
596
		$user_tokens     = \Jetpack_Options::get_option( 'user_tokens' );
597
598
		if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) {
599
			return $connected_users;
600
		}
601
		$connected_user_ids = array_keys( $user_tokens );
602
603
		if ( ! empty( $connected_user_ids ) ) {
604
			foreach ( $connected_user_ids as $id ) {
605
				// Check for capability.
606
				if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
607
					continue;
608
				}
609
610
				$user_data = get_userdata( $id );
611
				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...
612
					$connected_users[] = $user_data;
613
				}
614
			}
615
		}
616
617
		return $connected_users;
618
	}
619
620
	/**
621
	 * Fetches a signed token.
622
	 *
623
	 * @param object $token the token.
624
	 * @return WP_Error|string a signed token
625
	 */
626
	public static function get_signed_token( $token ) {
627
		if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
628
			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...
629
		}
630
631
		list( $token_key, $token_secret ) = explode( '.', $token->secret );
632
633
		$token_key = sprintf(
634
			'%s:%d:%d',
635
			$token_key,
636
			Constants::get_constant( 'JETPACK__API_VERSION' ),
637
			$token->external_user_id
638
		);
639
640
		$timestamp = time();
641
642 View Code Duplication
		if ( function_exists( 'wp_generate_password' ) ) {
643
			$nonce = wp_generate_password( 10, false );
644
		} else {
645
			$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
646
		}
647
648
		$normalized_request_string = join(
649
			"\n",
650
			array(
651
				$token_key,
652
				$timestamp,
653
				$nonce,
654
			)
655
		) . "\n";
656
657
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
658
		$signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
659
660
		$auth = array(
661
			'token'     => $token_key,
662
			'timestamp' => $timestamp,
663
			'nonce'     => $nonce,
664
			'signature' => $signature,
665
		);
666
667
		$header_pieces = array();
668
		foreach ( $auth as $key => $value ) {
669
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
670
		}
671
672
		return join( ' ', $header_pieces );
673
	}
674
}
675