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