Completed
Push — try/refactor-secrets-and-token... ( ad6cd9...9cdaa1 )
by
unknown
837:53 queued 825:50
created

Tokens::update_blog_token()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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
	 * @return bool True if disconnected successfully, false otherwise.
26
	 */
27
	public function delete_all() {
28
		/**
29
		 * Fires upon the disconnect attempt.
30
		 * Return `false` to prevent the disconnect.
31
		 *
32
		 * @since 8.7.0
33
		 */
34
		if ( ! apply_filters( 'jetpack_connection_delete_all_tokens', true ) ) {
35
			return false;
36
		}
37
38
		\Jetpack_Options::delete_option(
39
			array(
40
				'blog_token',
41
				'user_token',
42
				'user_tokens',
43
				'master_user',
44
				'time_diff',
45
				'fallback_no_verify_ssl_certs',
46
			)
47
		);
48
49
		\Jetpack_Options::delete_raw_option( 'jetpack_secrets' );
50
51
		// Delete cached connected user data.
52
		$transient_key = 'jetpack_connected_user_data_' . get_current_user_id();
53
		delete_transient( $transient_key );
54
55
		// Delete all XML-RPC errors.
56
		Error_Handler::get_instance()->delete_all_errors();
57
58
		return true;
59
	}
60
61
	/**
62
	 * Perform the API request to validate the blog and user tokens.
63
	 *
64
	 * @param int|null $user_id ID of the user we need to validate token for. Current user's ID by default.
65
	 *
66
	 * @return array|false|WP_Error The API response: `array( 'blog_token_is_healthy' => true|false, 'user_token_is_healthy' => true|false )`.
67
	 */
68
	public function validate( $user_id = null ) {
69
		$blog_id = Jetpack_Options::get_option( 'id' );
70
		if ( ! $blog_id ) {
71
			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...
72
		}
73
		$url = sprintf(
74
			'%s/%s/v%s/%s',
75
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ),
76
			'wpcom',
77
			'2',
78
			'sites/' . $blog_id . '/jetpack-token-health'
79
		);
80
81
		$user_token = $this->get_access_token( $user_id ? $user_id : get_current_user_id() );
82
		$blog_token = $this->get_access_token();
83
		$method     = 'POST';
84
		$body       = array(
85
			'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 81 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...
86
			'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 82 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...
87
		);
88
		$response   = Client::_wp_remote_request( $url, compact( 'body', 'method' ) );
89
90
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_body( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
91
			return false;
92
		}
93
94
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
95
96
		return $body ? $body : false;
97
	}
98
99
	/**
100
	 * Obtains the auth token.
101
	 *
102
	 * @param array  $data The request data.
103
	 * @param string $token_api_url The URL of the Jetpack "token" API.
104
	 * @return object|\WP_Error Returns the auth token on success.
105
	 *                          Returns a \WP_Error on failure.
106
	 */
107
	public function get( $data, $token_api_url ) {
108
		$roles = new Roles();
109
		$role  = $roles->translate_current_user_to_role();
110
111
		if ( ! $role ) {
112
			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...
113
		}
114
115
		$client_secret = $this->get_access_token();
116
		if ( ! $client_secret ) {
117
			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...
118
		}
119
120
		/**
121
		 * Filter the URL of the first time the user gets redirected back to your site for connection
122
		 * data processing.
123
		 *
124
		 * @since 8.0.0
125
		 *
126
		 * @param string $redirect_url Defaults to the site admin URL.
127
		 */
128
		$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) );
129
130
		$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
131
132
		/**
133
		* Filter the URL to redirect the user back to when the authentication process
134
		* is complete.
135
		*
136
		* @since 8.0.0
137
		*
138
		* @param string $redirect_url Defaults to the site URL.
139
		*/
140
		$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect );
141
142
		$redirect_uri = ( 'calypso' === $data['auth_type'] )
143
			? $data['redirect_uri']
144
			: add_query_arg(
145
				array(
146
					'handler'  => 'jetpack-connection-webhooks',
147
					'action'   => 'authorize',
148
					'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ),
149
					'redirect' => $redirect ? rawurlencode( $redirect ) : false,
150
				),
151
				esc_url( $processing_url )
152
			);
153
154
		/**
155
		 * Filters the token request data.
156
		 *
157
		 * @since 8.0.0
158
		 *
159
		 * @param array $request_data request data.
160
		 */
161
		$body = apply_filters(
162
			'jetpack_token_request_body',
163
			array(
164
				'client_id'     => \Jetpack_Options::get_option( 'id' ),
165
				'client_secret' => $client_secret->secret,
166
				'grant_type'    => 'authorization_code',
167
				'code'          => $data['code'],
168
				'redirect_uri'  => $redirect_uri,
169
			)
170
		);
171
172
		$args = array(
173
			'method'  => 'POST',
174
			'body'    => $body,
175
			'headers' => array(
176
				'Accept' => 'application/json',
177
			),
178
		);
179
		add_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
180
		$response = Client::_wp_remote_request( $token_api_url, $args );
181
		remove_filter( 'http_request_timeout', array( $this, 'return_30' ), PHP_INT_MAX - 1 );
182
183
		if ( is_wp_error( $response ) ) {
184
			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...
185
		}
186
187
		$code   = wp_remote_retrieve_response_code( $response );
188
		$entity = wp_remote_retrieve_body( $response );
189
190
		if ( $entity ) {
191
			$json = json_decode( $entity );
192
		} else {
193
			$json = false;
194
		}
195
196 View Code Duplication
		if ( 200 !== $code || ! empty( $json->error ) ) {
197
			if ( empty( $json->error ) ) {
198
				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...
199
			}
200
201
			/* translators: Error description string. */
202
			$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : '';
203
204
			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...
205
		}
206
207
		if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) {
208
			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...
209
		}
210
211
		if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) {
212
			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...
213
		}
214
215
		if ( empty( $json->scope ) ) {
216
			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...
217
		}
218
219
		// TODO: get rid of the error silencer.
220
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
221
		@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...
222
		if ( empty( $role ) || empty( $hmac ) ) {
223
			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...
224
		}
225
226
		if ( $this->sign_role( $role ) !== $json->scope ) {
227
			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...
228
		}
229
230
		$cap = $roles->translate_role_to_cap( $role );
231
		if ( ! $cap ) {
232
			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...
233
		}
234
235
		if ( ! current_user_can( $cap ) ) {
236
			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...
237
		}
238
239
		return (string) $json->access_token;
240
	}
241
242
	/**
243
	 * Enters a user token into the user_tokens option
244
	 *
245
	 * @param int    $user_id The user id.
246
	 * @param string $token The user token.
247
	 * @param bool   $is_master_user Whether the user is the master user.
248
	 * @return bool
249
	 */
250
	public function update_user_token( $user_id, $token, $is_master_user ) {
251
		// Not designed for concurrent updates.
252
		$user_tokens = \Jetpack_Options::get_option( 'user_tokens' );
253
		if ( ! is_array( $user_tokens ) ) {
254
			$user_tokens = array();
255
		}
256
		$user_tokens[ $user_id ] = $token;
257
		if ( $is_master_user ) {
258
			$master_user = $user_id;
259
			$options     = compact( 'user_tokens', 'master_user' );
260
		} else {
261
			$options = compact( 'user_tokens' );
262
		}
263
		return \Jetpack_Options::update_options( $options );
264
	}
265
266
	/**
267
	 * Sign a user role with the master access token.
268
	 * If not specified, will default to the current user.
269
	 *
270
	 * @access public
271
	 *
272
	 * @param string $role    User role.
273
	 * @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...
274
	 * @return string Signed user role.
275
	 */
276
	public function sign_role( $role, $user_id = null ) {
277
		if ( empty( $user_id ) ) {
278
			$user_id = (int) get_current_user_id();
279
		}
280
281
		if ( ! $user_id ) {
282
			return false;
283
		}
284
285
		$token = $this->get_access_token();
286
		if ( ! $token || is_wp_error( $token ) ) {
287
			return false;
288
		}
289
290
		return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret );
291
	}
292
293
	/**
294
	 * Increases the request timeout value to 30 seconds.
295
	 *
296
	 * @return int Returns 30.
297
	 */
298
	public function return_30() {
299
		return 30;
300
	}
301
302
	/**
303
	 * Gets the requested token.
304
	 *
305
	 * Tokens are one of two types:
306
	 * 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token,
307
	 *    though some sites can have multiple "Special" Blog Tokens (see below). These tokens
308
	 *    are not associated with a user account. They represent the site's connection with
309
	 *    the Jetpack servers.
310
	 * 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token.
311
	 *
312
	 * All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the
313
	 * token, and $private is a secret that should never be displayed anywhere or sent
314
	 * over the network; it's used only for signing things.
315
	 *
316
	 * Blog Tokens can be "Normal" or "Special".
317
	 * * Normal: The result of a normal connection flow. They look like
318
	 *   "{$random_string_1}.{$random_string_2}"
319
	 *   That is, $token_key and $private are both random strings.
320
	 *   Sites only have one Normal Blog Token. Normal Tokens are found in either
321
	 *   Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN
322
	 *   constant (rare).
323
	 * * Special: A connection token for sites that have gone through an alternative
324
	 *   connection flow. They look like:
325
	 *   ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}"
326
	 *   That is, $private is a random string and $token_key has a special structure with
327
	 *   lots of semicolons.
328
	 *   Most sites have zero Special Blog Tokens. Special tokens are only found in the
329
	 *   JETPACK_BLOG_TOKEN constant.
330
	 *
331
	 * In particular, note that Normal Blog Tokens never start with ";" and that
332
	 * Special Blog Tokens always do.
333
	 *
334
	 * When searching for a matching Blog Tokens, Blog Tokens are examined in the following
335
	 * order:
336
	 * 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant)
337
	 * 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' ))
338
	 * 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant)
339
	 *
340
	 * @param int|false    $user_id   false: Return the Blog Token. int: Return that user's User Token.
341
	 * @param string|false $token_key If provided, check that the token matches the provided input.
342
	 * @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.
343
	 *
344
	 * @return object|false
345
	 */
346
	public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) {
347
		$possible_special_tokens = array();
348
		$possible_normal_tokens  = array();
349
		$user_tokens             = \Jetpack_Options::get_option( 'user_tokens' );
350
351
		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...
352
			if ( ! $user_tokens ) {
353
				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...
354
			}
355
			if ( true === $user_id ) { // connection owner.
356
				$user_id = \Jetpack_Options::get_option( 'master_user' );
357
				if ( ! $user_id ) {
358
					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...
359
				}
360
			}
361
			if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) {
362
				// translators: %s is the user ID.
363
				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...
364
			}
365
			$user_token_chunks = explode( '.', $user_tokens[ $user_id ] );
366 View Code Duplication
			if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) {
367
				// translators: %s is the user ID.
368
				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...
369
			}
370
			if ( $user_token_chunks[2] !== (string) $user_id ) {
371
				// translators: %1$d is the ID of the requested user. %2$d is the user ID found in the token.
372
				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...
373
			}
374
			$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}";
375
		} else {
376
			$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' );
377
			if ( $stored_blog_token ) {
378
				$possible_normal_tokens[] = $stored_blog_token;
379
			}
380
381
			$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' );
382
383
			if ( $defined_tokens_string ) {
384
				$defined_tokens = explode( ',', $defined_tokens_string );
385
				foreach ( $defined_tokens as $defined_token ) {
386
					if ( ';' === $defined_token[0] ) {
387
						$possible_special_tokens[] = $defined_token;
388
					} else {
389
						$possible_normal_tokens[] = $defined_token;
390
					}
391
				}
392
			}
393
		}
394
395
		if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
396
			$possible_tokens = $possible_normal_tokens;
397
		} else {
398
			$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens );
399
		}
400
401
		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...
402
			// If no user tokens were found, it would have failed earlier, so this is about blog token.
403
			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...
404
		}
405
406
		$valid_token = false;
407
408
		if ( false === $token_key ) {
409
			// Use first token.
410
			$valid_token = $possible_tokens[0];
411
		} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) {
412
			// Use first normal token.
413
			$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check.
414
		} else {
415
			// Use the token matching $token_key or false if none.
416
			// Ensure we check the full key.
417
			$token_check = rtrim( $token_key, '.' ) . '.';
418
419
			foreach ( $possible_tokens as $possible_token ) {
420
				if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) {
421
					$valid_token = $possible_token;
422
					break;
423
				}
424
			}
425
		}
426
427
		if ( ! $valid_token ) {
428
			if ( $user_id ) {
429
				// translators: %d is the user ID.
430
				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...
431
			} else {
432
				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...
433
			}
434
		}
435
436
		return (object) array(
437
			'secret'           => $valid_token,
438
			'external_user_id' => (int) $user_id,
439
		);
440
	}
441
442
	/**
443
	 * Updates the blog token to a new value.
444
	 *
445
	 * @access public
446
	 *
447
	 * @param string $token the new blog token value.
448
	 * @return Boolean Whether updating the blog token was successful.
449
	 */
450
	public function update_blog_token( $token ) {
451
		return Jetpack_Options::update_option( 'blog_token', $token );
452
	}
453
454
	/**
455
	 * Unlinks the current user from the linked WordPress.com user.
456
	 *
457
	 * @access public
458
	 * @static
459
	 *
460
	 * @todo Refactor to properly load the XMLRPC client independently.
461
	 *
462
	 * @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...
463
	 * @param bool    $can_overwrite_primary_user Allow for the primary user to be disconnected.
464
	 * @return Boolean Whether the disconnection of the user was successful.
465
	 */
466
	public function disconnect_user( $user_id = null, $can_overwrite_primary_user = false ) {
467
		$tokens = Jetpack_Options::get_option( 'user_tokens' );
468
		if ( ! $tokens ) {
469
			return false;
470
		}
471
472
		$user_id = empty( $user_id ) ? get_current_user_id() : (int) $user_id;
473
474
		if ( Jetpack_Options::get_option( 'master_user' ) === $user_id && ! $can_overwrite_primary_user ) {
475
			return false;
476
		}
477
478
		if ( ! isset( $tokens[ $user_id ] ) ) {
479
			return false;
480
		}
481
482
		$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) );
483
		$xml->query( 'jetpack.unlink_user', $user_id );
484
485
		unset( $tokens[ $user_id ] );
486
487
		Jetpack_Options::update_option( 'user_tokens', $tokens );
488
489
		// Delete cached connected user data.
490
		$transient_key = "jetpack_connected_user_data_$user_id";
491
		delete_transient( $transient_key );
492
493
		/**
494
		 * Fires after the current user has been unlinked from WordPress.com.
495
		 *
496
		 * @since 4.1.0
497
		 *
498
		 * @param int $user_id The current user's ID.
499
		 */
500
		do_action( 'jetpack_unlinked_user', $user_id );
501
502
		return true;
503
	}
504
505
	/**
506
	 * Returns an array of user_id's that have user tokens for communicating with wpcom.
507
	 * Able to select by specific capability.
508
	 *
509
	 * @param string $capability The capability of the user.
510
	 * @return array Array of WP_User objects if found.
511
	 */
512
	public function get_connected_users( $capability = 'any' ) {
513
		$connected_users = array();
514
		$user_tokens     = \Jetpack_Options::get_option( 'user_tokens' );
515
516
		if ( ! is_array( $user_tokens ) || empty( $user_tokens ) ) {
517
			return $connected_users;
518
		}
519
		$connected_user_ids = array_keys( $user_tokens );
520
521
		if ( ! empty( $connected_user_ids ) ) {
522
			foreach ( $connected_user_ids as $id ) {
523
				// Check for capability.
524
				if ( 'any' !== $capability && ! user_can( $id, $capability ) ) {
525
					continue;
526
				}
527
528
				$user_data = get_userdata( $id );
529
				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...
530
					$connected_users[] = $user_data;
531
				}
532
			}
533
		}
534
535
		return $connected_users;
536
	}
537
538
	/**
539
	 * Fetches a signed token.
540
	 *
541
	 * @param object $token the token.
542
	 * @return WP_Error|string a signed token
543
	 */
544
	public function get_signed_token( $token ) {
545
		if ( ! isset( $token->secret ) || empty( $token->secret ) ) {
546
			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...
547
		}
548
549
		list( $token_key, $token_secret ) = explode( '.', $token->secret );
550
551
		$token_key = sprintf(
552
			'%s:%d:%d',
553
			$token_key,
554
			Constants::get_constant( 'JETPACK__API_VERSION' ),
555
			$token->external_user_id
556
		);
557
558
		$timestamp = time();
559
560 View Code Duplication
		if ( function_exists( 'wp_generate_password' ) ) {
561
			$nonce = wp_generate_password( 10, false );
562
		} else {
563
			$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
564
		}
565
566
		$normalized_request_string = join(
567
			"\n",
568
			array(
569
				$token_key,
570
				$timestamp,
571
				$nonce,
572
			)
573
		) . "\n";
574
575
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
576
		$signature = base64_encode( hash_hmac( 'sha1', $normalized_request_string, $token_secret, true ) );
577
578
		$auth = array(
579
			'token'     => $token_key,
580
			'timestamp' => $timestamp,
581
			'nonce'     => $nonce,
582
			'signature' => $signature,
583
		);
584
585
		$header_pieces = array();
586
		foreach ( $auth as $key => $value ) {
587
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
588
		}
589
590
		return join( ' ', $header_pieces );
591
	}
592
}
593