1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* The Jetpack Connection manager 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
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* The Jetpack Connection Manager class that is used as a single gateway between WordPress.com |
16
|
|
|
* and Jetpack. |
17
|
|
|
*/ |
18
|
|
|
class Manager { |
19
|
|
|
|
20
|
|
|
const SECRETS_MISSING = 'secrets_missing'; |
21
|
|
|
const SECRETS_EXPIRED = 'secrets_expired'; |
22
|
|
|
const SECRETS_OPTION_NAME = 'jetpack_secrets'; |
23
|
|
|
const MAGIC_NORMAL_TOKEN_KEY = ';normal;'; |
24
|
|
|
const JETPACK_MASTER_USER = true; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The procedure that should be run to generate secrets. |
28
|
|
|
* |
29
|
|
|
* @var Callable |
30
|
|
|
*/ |
31
|
|
|
protected $secret_callable; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* A copy of the raw POST data for signature verification purposes. |
35
|
|
|
* |
36
|
|
|
* @var String |
37
|
|
|
*/ |
38
|
|
|
protected $raw_post_data; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Verification data needs to be stored to properly verify everything. |
42
|
|
|
* |
43
|
|
|
* @var Object |
44
|
|
|
*/ |
45
|
|
|
private $xmlrpc_verification = null; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Initializes required listeners. This is done separately from the constructors |
49
|
|
|
* because some objects sometimes need to instantiate separate objects of this class. |
50
|
|
|
* |
51
|
|
|
* @todo Implement a proper nonce verification. |
52
|
|
|
*/ |
53
|
|
|
public static function configure() { |
54
|
|
|
$manager = new self(); |
55
|
|
|
|
56
|
|
|
$manager->setup_xmlrpc_handlers( |
57
|
|
|
$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
58
|
|
|
$manager->is_active(), |
59
|
|
|
$manager->verify_xml_rpc_signature() |
|
|
|
|
60
|
|
|
); |
61
|
|
|
|
62
|
|
|
if ( $manager->is_active() ) { |
63
|
|
|
add_filter( 'xmlrpc_methods', array( $manager, 'public_xmlrpc_methods' ) ); |
64
|
|
|
} else { |
65
|
|
|
add_action( 'rest_api_init', array( $manager, 'initialize_rest_api_registration_connector' ) ); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
add_action( 'jetpack_clean_nonces', array( $manager, 'clean_nonces' ) ); |
69
|
|
|
if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { |
70
|
|
|
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); |
71
|
|
|
} |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Sets up the XMLRPC request handlers. |
76
|
|
|
* |
77
|
|
|
* @param Array $request_params incoming request parameters. |
78
|
|
|
* @param Boolean $is_active whether the connection is currently active. |
79
|
|
|
* @param Boolean $is_signed whether the signature check has been successful. |
80
|
|
|
* @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one. |
|
|
|
|
81
|
|
|
*/ |
82
|
|
|
public function setup_xmlrpc_handlers( |
83
|
|
|
$request_params, |
84
|
|
|
$is_active, |
85
|
|
|
$is_signed, |
86
|
|
|
\Jetpack_XMLRPC_Server $xmlrpc_server = null |
87
|
|
|
) { |
88
|
|
|
add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ), 1000, 2 ); |
89
|
|
|
|
90
|
|
|
if ( |
91
|
|
|
! isset( $request_params['for'] ) |
92
|
|
|
|| 'jetpack' !== $request_params['for'] |
93
|
|
|
) { |
94
|
|
|
return false; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
// Alternate XML-RPC, via ?for=jetpack&jetpack=comms. |
98
|
|
|
if ( |
99
|
|
|
isset( $request_params['jetpack'] ) |
100
|
|
|
&& 'comms' === $request_params['jetpack'] |
101
|
|
|
) { |
102
|
|
|
if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) { |
103
|
|
|
// Use the real constant here for WordPress' sake. |
104
|
|
|
define( 'XMLRPC_REQUEST', true ); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) ); |
108
|
|
|
|
109
|
|
|
add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 ); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) { |
113
|
|
|
return false; |
114
|
|
|
} |
115
|
|
|
// Display errors can cause the XML to be not well formed. |
116
|
|
|
@ini_set( 'display_errors', false ); // phpcs:ignore |
|
|
|
|
117
|
|
|
|
118
|
|
|
if ( $xmlrpc_server ) { |
119
|
|
|
$this->xmlrpc_server = $xmlrpc_server; |
|
|
|
|
120
|
|
|
} else { |
121
|
|
|
$this->xmlrpc_server = new \Jetpack_XMLRPC_Server(); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$this->require_jetpack_authentication(); |
125
|
|
|
|
126
|
|
|
if ( $is_active ) { |
127
|
|
|
// Hack to preserve $HTTP_RAW_POST_DATA. |
128
|
|
|
add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); |
129
|
|
|
|
130
|
|
|
if ( $is_signed ) { |
131
|
|
|
// The actual API methods. |
132
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) ); |
133
|
|
|
} else { |
134
|
|
|
// The jetpack.authorize method should be available for unauthenticated users on a site with an |
135
|
|
|
// active Jetpack connection, so that additional users can link their account. |
136
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) ); |
137
|
|
|
} |
138
|
|
|
} else { |
139
|
|
|
// The bootstrap API methods. |
140
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) ); |
141
|
|
|
|
142
|
|
|
if ( $is_signed ) { |
143
|
|
|
// The jetpack Provision method is available for blog-token-signed requests. |
144
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) ); |
145
|
|
|
} else { |
146
|
|
|
new XMLRPC_Connector( $this ); |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
// Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on. |
151
|
|
|
add_filter( 'pre_option_enable_xmlrpc', '__return_true' ); |
152
|
|
|
return true; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Initializes the REST API connector on the init hook. |
157
|
|
|
*/ |
158
|
|
|
public function initialize_rest_api_registration_connector() { |
159
|
|
|
new REST_Connector( $this ); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Since a lot of hosts use a hammer approach to "protecting" WordPress sites, |
164
|
|
|
* and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive |
165
|
|
|
* security/firewall policies, we provide our own alternate XML RPC API endpoint |
166
|
|
|
* which is accessible via a different URI. Most of the below is copied directly |
167
|
|
|
* from /xmlrpc.php so that we're replicating it as closely as possible. |
168
|
|
|
* |
169
|
|
|
* @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things. |
170
|
|
|
*/ |
171
|
|
|
public function alternate_xmlrpc() { |
172
|
|
|
// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved |
173
|
|
|
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited |
174
|
|
|
global $HTTP_RAW_POST_DATA; |
175
|
|
|
|
176
|
|
|
// Some browser-embedded clients send cookies. We don't want them. |
177
|
|
|
$_COOKIE = array(); |
178
|
|
|
|
179
|
|
|
// A fix for mozBlog and other cases where '<?xml' isn't on the very first line. |
180
|
|
|
if ( isset( $HTTP_RAW_POST_DATA ) ) { |
181
|
|
|
$HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA ); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
// phpcs:enable |
185
|
|
|
|
186
|
|
|
include_once ABSPATH . 'wp-admin/includes/admin.php'; |
187
|
|
|
include_once ABSPATH . WPINC . '/class-IXR.php'; |
188
|
|
|
include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'; |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Filters the class used for handling XML-RPC requests. |
192
|
|
|
* |
193
|
|
|
* @since 3.1.0 |
194
|
|
|
* |
195
|
|
|
* @param string $class The name of the XML-RPC server class. |
196
|
|
|
*/ |
197
|
|
|
$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' ); |
198
|
|
|
$wp_xmlrpc_server = new $wp_xmlrpc_server_class(); |
199
|
|
|
|
200
|
|
|
// Fire off the request. |
201
|
|
|
nocache_headers(); |
202
|
|
|
$wp_xmlrpc_server->serve_request(); |
203
|
|
|
|
204
|
|
|
exit; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Removes all XML-RPC methods that are not `jetpack.*`. |
209
|
|
|
* Only used in our alternate XML-RPC endpoint, where we want to |
210
|
|
|
* ensure that Core and other plugins' methods are not exposed. |
211
|
|
|
* |
212
|
|
|
* @param array $methods a list of registered WordPress XMLRPC methods. |
213
|
|
|
* @return array filtered $methods |
214
|
|
|
*/ |
215
|
|
|
public function remove_non_jetpack_xmlrpc_methods( $methods ) { |
216
|
|
|
$jetpack_methods = array(); |
217
|
|
|
|
218
|
|
|
foreach ( $methods as $method => $callback ) { |
219
|
|
|
if ( 0 === strpos( $method, 'jetpack.' ) ) { |
220
|
|
|
$jetpack_methods[ $method ] = $callback; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
return $jetpack_methods; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Removes all other authentication methods not to allow other |
229
|
|
|
* methods to validate unauthenticated requests. |
230
|
|
|
*/ |
231
|
|
|
public function require_jetpack_authentication() { |
232
|
|
|
// Don't let anyone authenticate. |
233
|
|
|
$_COOKIE = array(); |
234
|
|
|
remove_all_filters( 'authenticate' ); |
235
|
|
|
remove_all_actions( 'wp_login_failed' ); |
236
|
|
|
|
237
|
|
|
if ( $this->is_active() ) { |
238
|
|
|
// Allow Jetpack authentication. |
239
|
|
|
add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 ); |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Authenticates XML-RPC and other requests from the Jetpack Server |
245
|
|
|
* |
246
|
|
|
* @param WP_User|Mixed $user user object if authenticated. |
247
|
|
|
* @param String $username username. |
248
|
|
|
* @param String $password password string. |
249
|
|
|
* @return WP_User|Mixed authenticated user or error. |
250
|
|
|
*/ |
251
|
|
|
public function authenticate_jetpack( $user, $username, $password ) { |
252
|
|
|
if ( is_a( $user, '\\WP_User' ) ) { |
253
|
|
|
return $user; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
$token_details = $this->verify_xml_rpc_signature(); |
257
|
|
|
|
258
|
|
|
if ( ! $token_details ) { |
259
|
|
|
return $user; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
if ( 'user' !== $token_details['type'] ) { |
263
|
|
|
return $user; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
if ( ! $token_details['user_id'] ) { |
267
|
|
|
return $user; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
nocache_headers(); |
271
|
|
|
|
272
|
|
|
return new \WP_User( $token_details['user_id'] ); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Verifies the signature of the current request. |
277
|
|
|
* |
278
|
|
|
* @return false|array |
279
|
|
|
*/ |
280
|
|
|
public function verify_xml_rpc_signature() { |
281
|
|
|
if ( is_null( $this->xmlrpc_verification ) ) { |
282
|
|
|
$this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature(); |
283
|
|
|
|
284
|
|
|
if ( is_wp_error( $this->xmlrpc_verification ) ) { |
285
|
|
|
/** |
286
|
|
|
* Action for logging XMLRPC signature verification errors. This data is sensitive. |
287
|
|
|
* |
288
|
|
|
* Error codes: |
289
|
|
|
* - malformed_token |
290
|
|
|
* - malformed_user_id |
291
|
|
|
* - unknown_token |
292
|
|
|
* - could_not_sign |
293
|
|
|
* - invalid_nonce |
294
|
|
|
* - signature_mismatch |
295
|
|
|
* |
296
|
|
|
* @since 7.5.0 |
297
|
|
|
* |
298
|
|
|
* @param WP_Error $signature_verification_error The verification error |
299
|
|
|
*/ |
300
|
|
|
do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification ); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Verifies the signature of the current request. |
309
|
|
|
* |
310
|
|
|
* This function has side effects and should not be used. Instead, |
311
|
|
|
* use the memoized version `->verify_xml_rpc_signature()`. |
312
|
|
|
* |
313
|
|
|
* @internal |
314
|
|
|
* @todo Refactor to use proper nonce verification. |
315
|
|
|
*/ |
316
|
|
|
private function internal_verify_xml_rpc_signature() { |
317
|
|
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended |
318
|
|
|
// It's not for us. |
319
|
|
|
if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) { |
320
|
|
|
return false; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$signature_details = array( |
324
|
|
|
'token' => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '', |
325
|
|
|
'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '', |
326
|
|
|
'nonce' => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '', |
327
|
|
|
'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '', |
328
|
|
|
'method' => wp_unslash( $_SERVER['REQUEST_METHOD'] ), |
329
|
|
|
'url' => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later. |
330
|
|
|
'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', |
331
|
|
|
); |
332
|
|
|
|
333
|
|
|
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged |
334
|
|
|
@list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); |
|
|
|
|
335
|
|
|
// phpcs:enable WordPress.Security.NonceVerification.Recommended |
336
|
|
|
|
337
|
|
|
if ( |
338
|
|
|
empty( $token_key ) |
339
|
|
|
|| |
340
|
|
|
empty( $version ) || strval( Utils::get_jetpack_api_version() ) !== $version |
341
|
|
|
) { |
342
|
|
|
return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) ); |
|
|
|
|
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
if ( '0' === $user_id ) { |
346
|
|
|
$token_type = 'blog'; |
347
|
|
|
$user_id = 0; |
348
|
|
|
} else { |
349
|
|
|
$token_type = 'user'; |
350
|
|
|
if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) { |
351
|
|
|
return new \WP_Error( |
352
|
|
|
'malformed_user_id', |
|
|
|
|
353
|
|
|
'Malformed user_id in request', |
354
|
|
|
compact( 'signature_details' ) |
355
|
|
|
); |
356
|
|
|
} |
357
|
|
|
$user_id = (int) $user_id; |
358
|
|
|
|
359
|
|
|
$user = new \WP_User( $user_id ); |
360
|
|
|
if ( ! $user || ! $user->exists() ) { |
361
|
|
|
return new \WP_Error( |
362
|
|
|
'unknown_user', |
|
|
|
|
363
|
|
|
sprintf( 'User %d does not exist', $user_id ), |
364
|
|
|
compact( 'signature_details' ) |
365
|
|
|
); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
$token = $this->get_access_token( $user_id, $token_key, false ); |
370
|
|
|
if ( is_wp_error( $token ) ) { |
371
|
|
|
$token->add_data( compact( 'signature_details' ) ); |
372
|
|
|
return $token; |
373
|
|
|
} elseif ( ! $token ) { |
374
|
|
|
return new \WP_Error( |
375
|
|
|
'unknown_token', |
|
|
|
|
376
|
|
|
sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ), |
377
|
|
|
compact( 'signature_details' ) |
378
|
|
|
); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); |
382
|
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing |
383
|
|
|
if ( isset( $_POST['_jetpack_is_multipart'] ) ) { |
384
|
|
|
$post_data = $_POST; |
385
|
|
|
$file_hashes = array(); |
386
|
|
|
foreach ( $post_data as $post_data_key => $post_data_value ) { |
387
|
|
|
if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) { |
388
|
|
|
continue; |
389
|
|
|
} |
390
|
|
|
$post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) ); |
391
|
|
|
$file_hashes[ $post_data_key ] = $post_data_value; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
foreach ( $file_hashes as $post_data_key => $post_data_value ) { |
395
|
|
|
unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] ); |
396
|
|
|
$post_data[ $post_data_key ] = $post_data_value; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
ksort( $post_data ); |
400
|
|
|
|
401
|
|
|
$body = http_build_query( stripslashes_deep( $post_data ) ); |
402
|
|
|
} elseif ( is_null( $this->raw_post_data ) ) { |
403
|
|
|
$body = file_get_contents( 'php://input' ); |
404
|
|
|
} else { |
405
|
|
|
$body = null; |
406
|
|
|
} |
407
|
|
|
// phpcs:enable |
408
|
|
|
|
409
|
|
|
$signature = $jetpack_signature->sign_current_request( |
410
|
|
|
array( 'body' => is_null( $body ) ? $this->raw_post_data : $body ) |
411
|
|
|
); |
412
|
|
|
|
413
|
|
|
$signature_details['url'] = $jetpack_signature->current_request_url; |
414
|
|
|
|
415
|
|
|
if ( ! $signature ) { |
416
|
|
|
return new \WP_Error( |
417
|
|
|
'could_not_sign', |
|
|
|
|
418
|
|
|
'Unknown signature error', |
419
|
|
|
compact( 'signature_details' ) |
420
|
|
|
); |
421
|
|
|
} elseif ( is_wp_error( $signature ) ) { |
422
|
|
|
return $signature; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended |
426
|
|
|
$timestamp = (int) $_GET['timestamp']; |
427
|
|
|
$nonce = stripslashes( (string) $_GET['nonce'] ); |
428
|
|
|
// phpcs:enable WordPress.Security.NonceVerification.Recommended |
429
|
|
|
|
430
|
|
|
// Use up the nonce regardless of whether the signature matches. |
431
|
|
|
if ( ! $this->add_nonce( $timestamp, $nonce ) ) { |
432
|
|
|
return new \WP_Error( |
433
|
|
|
'invalid_nonce', |
|
|
|
|
434
|
|
|
'Could not add nonce', |
435
|
|
|
compact( 'signature_details' ) |
436
|
|
|
); |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
// Be careful about what you do with this debugging data. |
440
|
|
|
// If a malicious requester has access to the expected signature, |
441
|
|
|
// bad things might be possible. |
442
|
|
|
$signature_details['expected'] = $signature; |
443
|
|
|
|
444
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
445
|
|
|
if ( ! hash_equals( $signature, $_GET['signature'] ) ) { |
446
|
|
|
return new \WP_Error( |
447
|
|
|
'signature_mismatch', |
|
|
|
|
448
|
|
|
'Signature mismatch', |
449
|
|
|
compact( 'signature_details' ) |
450
|
|
|
); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
/** |
454
|
|
|
* Action for additional token checking. |
455
|
|
|
* |
456
|
|
|
* @since 7.7.0 |
457
|
|
|
* |
458
|
|
|
* @param Array $post_data request data. |
459
|
|
|
* @param Array $token_data token data. |
460
|
|
|
*/ |
461
|
|
|
return apply_filters( |
462
|
|
|
'jetpack_signature_check_token', |
463
|
|
|
array( |
464
|
|
|
'type' => $token_type, |
465
|
|
|
'token_key' => $token_key, |
466
|
|
|
'user_id' => $token->external_user_id, |
467
|
|
|
), |
468
|
|
|
$token, |
469
|
|
|
$this->raw_post_data |
470
|
|
|
); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Returns true if the current site is connected to WordPress.com. |
475
|
|
|
* |
476
|
|
|
* @return Boolean is the site connected? |
477
|
|
|
*/ |
478
|
|
|
public function is_active() { |
479
|
|
|
return (bool) $this->get_access_token( self::JETPACK_MASTER_USER ); |
|
|
|
|
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Returns true if the site has both a token and a blog id, which indicates a site has been registered. |
484
|
|
|
* |
485
|
|
|
* @access public |
486
|
|
|
* |
487
|
|
|
* @return bool |
488
|
|
|
*/ |
489
|
|
|
public function is_registered() { |
490
|
|
|
$blog_id = \Jetpack_Options::get_option( 'id' ); |
491
|
|
|
$has_token = $this->is_active(); |
492
|
|
|
return $blog_id && $has_token; |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Checks to see if the connection owner of the site is missing. |
497
|
|
|
* |
498
|
|
|
* @return bool |
499
|
|
|
*/ |
500
|
|
|
public function is_missing_connection_owner() { |
501
|
|
|
$connection_owner = $this->get_connection_owner_id(); |
502
|
|
|
if ( ! get_user_by( 'id', $connection_owner ) ) { |
503
|
|
|
return true; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
return false; |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Returns true if the user with the specified identifier is connected to |
511
|
|
|
* WordPress.com. |
512
|
|
|
* |
513
|
|
|
* @param Integer|Boolean $user_id the user identifier. |
514
|
|
|
* @return Boolean is the user connected? |
515
|
|
|
*/ |
516
|
|
|
public function is_user_connected( $user_id = false ) { |
517
|
|
|
$user_id = false === $user_id ? get_current_user_id() : absint( $user_id ); |
518
|
|
|
if ( ! $user_id ) { |
519
|
|
|
return false; |
520
|
|
|
} |
521
|
|
|
|
522
|
|
|
return (bool) $this->get_access_token( $user_id ); |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* Returns the local user ID of the connection owner. |
527
|
|
|
* |
528
|
|
|
* @return string|int Returns the ID of the connection owner or False if no connection owner found. |
529
|
|
|
*/ |
530
|
|
View Code Duplication |
public function get_connection_owner_id() { |
531
|
|
|
$user_token = $this->get_access_token( self::JETPACK_MASTER_USER ); |
|
|
|
|
532
|
|
|
$connection_owner = false; |
533
|
|
|
if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { |
534
|
|
|
$connection_owner = $user_token->external_user_id; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
return $connection_owner; |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
/** |
541
|
|
|
* Returns an array of user_id's that have user tokens for communicating with wpcom. |
542
|
|
|
* Able to select by specific capability. |
543
|
|
|
* |
544
|
|
|
* @param string $capability The capability of the user. |
545
|
|
|
* @return array Array of WP_User objects if found. |
546
|
|
|
*/ |
547
|
|
|
public function get_connected_users( $capability = 'any' ) { |
548
|
|
|
$connected_users = array(); |
549
|
|
|
$connected_user_ids = array_keys( \Jetpack_Options::get_option( 'user_tokens' ) ); |
550
|
|
|
|
551
|
|
|
if ( ! empty( $connected_user_ids ) ) { |
552
|
|
|
foreach ( $connected_user_ids as $id ) { |
553
|
|
|
// Check for capability. |
554
|
|
|
if ( 'any' !== $capability && ! user_can( $id, $capability ) ) { |
555
|
|
|
continue; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
$connected_users[] = get_userdata( $id ); |
559
|
|
|
} |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
return $connected_users; |
563
|
|
|
} |
564
|
|
|
|
565
|
|
|
/** |
566
|
|
|
* Get the wpcom user data of the current|specified connected user. |
567
|
|
|
* |
568
|
|
|
* @todo Refactor to properly load the XMLRPC client independently. |
569
|
|
|
* |
570
|
|
|
* @param Integer $user_id the user identifier. |
|
|
|
|
571
|
|
|
* @return Object the user object. |
572
|
|
|
*/ |
573
|
|
View Code Duplication |
public function get_connected_user_data( $user_id = null ) { |
574
|
|
|
if ( ! $user_id ) { |
|
|
|
|
575
|
|
|
$user_id = get_current_user_id(); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$transient_key = "jetpack_connected_user_data_$user_id"; |
579
|
|
|
$cached_user_data = get_transient( $transient_key ); |
580
|
|
|
|
581
|
|
|
if ( $cached_user_data ) { |
582
|
|
|
return $cached_user_data; |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
$xml = new \Jetpack_IXR_Client( |
586
|
|
|
array( |
587
|
|
|
'user_id' => $user_id, |
588
|
|
|
) |
589
|
|
|
); |
590
|
|
|
$xml->query( 'wpcom.getUser' ); |
591
|
|
|
if ( ! $xml->isError() ) { |
592
|
|
|
$user_data = $xml->getResponse(); |
593
|
|
|
set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS ); |
594
|
|
|
return $user_data; |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
return false; |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* Returns a user object of the connection owner. |
602
|
|
|
* |
603
|
|
|
* @return object|false False if no connection owner found. |
604
|
|
|
*/ |
605
|
|
View Code Duplication |
public function get_connection_owner() { |
606
|
|
|
$user_token = $this->get_access_token( self::JETPACK_MASTER_USER ); |
|
|
|
|
607
|
|
|
|
608
|
|
|
$connection_owner = false; |
609
|
|
|
if ( $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) ) { |
610
|
|
|
$connection_owner = get_userdata( $user_token->external_user_id ); |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
return $connection_owner; |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* Returns true if the provided user is the Jetpack connection owner. |
618
|
|
|
* If user ID is not specified, the current user will be used. |
619
|
|
|
* |
620
|
|
|
* @param Integer|Boolean $user_id the user identifier. False for current user. |
621
|
|
|
* @return Boolean True the user the connection owner, false otherwise. |
622
|
|
|
*/ |
623
|
|
View Code Duplication |
public function is_connection_owner( $user_id = false ) { |
624
|
|
|
if ( ! $user_id ) { |
625
|
|
|
$user_id = get_current_user_id(); |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
$user_token = $this->get_access_token( self::JETPACK_MASTER_USER ); |
|
|
|
|
629
|
|
|
|
630
|
|
|
return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Connects the user with a specified ID to a WordPress.com user using the |
635
|
|
|
* remote login flow. |
636
|
|
|
* |
637
|
|
|
* @access public |
638
|
|
|
* |
639
|
|
|
* @param Integer $user_id (optional) the user identifier, defaults to current user. |
|
|
|
|
640
|
|
|
* @param String $redirect_url the URL to redirect the user to for processing, defaults to |
|
|
|
|
641
|
|
|
* admin_url(). |
642
|
|
|
* @return WP_Error only in case of a failed user lookup. |
643
|
|
|
*/ |
644
|
|
|
public function connect_user( $user_id = null, $redirect_url = null ) { |
645
|
|
|
$user = null; |
|
|
|
|
646
|
|
|
if ( null === $user_id ) { |
647
|
|
|
$user = wp_get_current_user(); |
648
|
|
|
} else { |
649
|
|
|
$user = get_user_by( 'ID', $user_id ); |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
if ( empty( $user ) ) { |
653
|
|
|
return new \WP_Error( 'user_not_found', 'Attempting to connect a non-existent user.' ); |
|
|
|
|
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
if ( null === $redirect_url ) { |
657
|
|
|
$redirect_url = admin_url(); |
|
|
|
|
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
// Using wp_redirect intentionally because we're redirecting outside. |
661
|
|
|
wp_redirect( $this->get_authorization_url( $user ) ); // phpcs:ignore WordPress.Security.SafeRedirect |
662
|
|
|
exit(); |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
/** |
666
|
|
|
* Unlinks the current user from the linked WordPress.com user. |
667
|
|
|
* |
668
|
|
|
* @access public |
669
|
|
|
* @static |
670
|
|
|
* |
671
|
|
|
* @todo Refactor to properly load the XMLRPC client independently. |
672
|
|
|
* |
673
|
|
|
* @param Integer $user_id the user identifier. |
|
|
|
|
674
|
|
|
* @return Boolean Whether the disconnection of the user was successful. |
675
|
|
|
*/ |
676
|
|
|
public static function disconnect_user( $user_id = null ) { |
677
|
|
|
$tokens = \Jetpack_Options::get_option( 'user_tokens' ); |
678
|
|
|
if ( ! $tokens ) { |
679
|
|
|
return false; |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
$user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id ); |
683
|
|
|
|
684
|
|
|
if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) { |
685
|
|
|
return false; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
if ( ! isset( $tokens[ $user_id ] ) ) { |
689
|
|
|
return false; |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) ); |
693
|
|
|
$xml->query( 'jetpack.unlink_user', $user_id ); |
694
|
|
|
|
695
|
|
|
unset( $tokens[ $user_id ] ); |
696
|
|
|
|
697
|
|
|
\Jetpack_Options::update_option( 'user_tokens', $tokens ); |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* Fires after the current user has been unlinked from WordPress.com. |
701
|
|
|
* |
702
|
|
|
* @since 4.1.0 |
703
|
|
|
* |
704
|
|
|
* @param int $user_id The current user's ID. |
705
|
|
|
*/ |
706
|
|
|
do_action( 'jetpack_unlinked_user', $user_id ); |
707
|
|
|
|
708
|
|
|
return true; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
/** |
712
|
|
|
* Returns the requested Jetpack API URL. |
713
|
|
|
* |
714
|
|
|
* @param String $relative_url the relative API path. |
715
|
|
|
* @return String API URL. |
716
|
|
|
*/ |
717
|
|
|
public function api_url( $relative_url ) { |
718
|
|
|
$api_base = Constants::get_constant( 'JETPACK__API_BASE' ); |
719
|
|
|
$api_base = $api_base ? $api_base : 'https://jetpack.wordpress.com/jetpack.'; |
720
|
|
|
$version = '/' . Utils::get_jetpack_api_version() . '/'; |
721
|
|
|
|
722
|
|
|
/** |
723
|
|
|
* Filters whether the connection manager should use the iframe authorization |
724
|
|
|
* flow instead of the regular redirect-based flow. |
725
|
|
|
* |
726
|
|
|
* @since 8.3.0 |
727
|
|
|
* |
728
|
|
|
* @param Boolean $is_iframe_flow_used should the iframe flow be used, defaults to false. |
729
|
|
|
*/ |
730
|
|
|
$iframe_flow = apply_filters( 'jetpack_use_iframe_authorization_flow', false ); |
731
|
|
|
|
732
|
|
|
// Do not modify anything that is not related to authorize requests. |
733
|
|
|
if ( 'authorize' === $relative_url && $iframe_flow ) { |
734
|
|
|
$relative_url = 'authorize_iframe'; |
735
|
|
|
} |
736
|
|
|
|
737
|
|
|
/** |
738
|
|
|
* Filters the API URL that Jetpack uses for server communication. |
739
|
|
|
* |
740
|
|
|
* @since 8.0.0 |
741
|
|
|
* |
742
|
|
|
* @param String $url the generated URL. |
743
|
|
|
* @param String $relative_url the relative URL that was passed as an argument. |
744
|
|
|
* @param String $api_base the API base string that is being used. |
745
|
|
|
* @param String $version the version string that is being used. |
746
|
|
|
*/ |
747
|
|
|
return apply_filters( |
748
|
|
|
'jetpack_api_url', |
749
|
|
|
rtrim( $api_base . $relative_url, '/\\' ) . $version, |
750
|
|
|
$relative_url, |
751
|
|
|
$api_base, |
752
|
|
|
$version |
753
|
|
|
); |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
/** |
757
|
|
|
* Returns the Jetpack XMLRPC WordPress.com API endpoint URL. |
758
|
|
|
* |
759
|
|
|
* @return String XMLRPC API URL. |
760
|
|
|
*/ |
761
|
|
|
public function xmlrpc_api_url() { |
762
|
|
|
$base = preg_replace( |
763
|
|
|
'#(https?://[^?/]+)(/?.*)?$#', |
764
|
|
|
'\\1', |
765
|
|
|
Constants::get_constant( 'JETPACK__API_BASE' ) |
766
|
|
|
); |
767
|
|
|
return untrailingslashit( $base ) . '/xmlrpc.php'; |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
/** |
771
|
|
|
* Attempts Jetpack registration which sets up the site for connection. Should |
772
|
|
|
* remain public because the call to action comes from the current site, not from |
773
|
|
|
* WordPress.com. |
774
|
|
|
* |
775
|
|
|
* @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'. |
776
|
|
|
* @return Integer zero on success, or a bitmask on failure. |
777
|
|
|
*/ |
778
|
|
|
public function register( $api_endpoint = 'register' ) { |
779
|
|
|
add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); |
780
|
|
|
$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 ); |
781
|
|
|
|
782
|
|
|
if ( |
783
|
|
|
empty( $secrets['secret_1'] ) || |
784
|
|
|
empty( $secrets['secret_2'] ) || |
785
|
|
|
empty( $secrets['exp'] ) |
786
|
|
|
) { |
787
|
|
|
return new \WP_Error( 'missing_secrets' ); |
|
|
|
|
788
|
|
|
} |
789
|
|
|
|
790
|
|
|
// Better to try (and fail) to set a higher timeout than this system |
791
|
|
|
// supports than to have register fail for more users than it should. |
792
|
|
|
$timeout = $this->set_min_time_limit( 60 ) / 2; |
793
|
|
|
|
794
|
|
|
$gmt_offset = get_option( 'gmt_offset' ); |
795
|
|
|
if ( ! $gmt_offset ) { |
796
|
|
|
$gmt_offset = 0; |
797
|
|
|
} |
798
|
|
|
|
799
|
|
|
$stats_options = get_option( 'stats_options' ); |
800
|
|
|
$stats_id = isset( $stats_options['blog_id'] ) |
801
|
|
|
? $stats_options['blog_id'] |
802
|
|
|
: null; |
803
|
|
|
|
804
|
|
|
/** |
805
|
|
|
* Filters the request body for additional property addition. |
806
|
|
|
* |
807
|
|
|
* @since 7.7.0 |
808
|
|
|
* |
809
|
|
|
* @param Array $post_data request data. |
810
|
|
|
* @param Array $token_data token data. |
811
|
|
|
*/ |
812
|
|
|
$body = apply_filters( |
813
|
|
|
'jetpack_register_request_body', |
814
|
|
|
array( |
815
|
|
|
'siteurl' => site_url(), |
816
|
|
|
'home' => home_url(), |
817
|
|
|
'gmt_offset' => $gmt_offset, |
818
|
|
|
'timezone_string' => (string) get_option( 'timezone_string' ), |
819
|
|
|
'site_name' => (string) get_option( 'blogname' ), |
820
|
|
|
'secret_1' => $secrets['secret_1'], |
821
|
|
|
'secret_2' => $secrets['secret_2'], |
822
|
|
|
'site_lang' => get_locale(), |
823
|
|
|
'timeout' => $timeout, |
824
|
|
|
'stats_id' => $stats_id, |
825
|
|
|
'state' => get_current_user_id(), |
826
|
|
|
'site_created' => $this->get_assumed_site_creation_date(), |
827
|
|
|
'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ), |
828
|
|
|
) |
829
|
|
|
); |
830
|
|
|
|
831
|
|
|
$args = array( |
832
|
|
|
'method' => 'POST', |
833
|
|
|
'body' => $body, |
834
|
|
|
'headers' => array( |
835
|
|
|
'Accept' => 'application/json', |
836
|
|
|
), |
837
|
|
|
'timeout' => $timeout, |
838
|
|
|
); |
839
|
|
|
|
840
|
|
|
$args['body'] = $this->apply_activation_source_to_args( $args['body'] ); |
841
|
|
|
|
842
|
|
|
// TODO: fix URLs for bad hosts. |
843
|
|
|
$response = Client::_wp_remote_request( |
844
|
|
|
$this->api_url( $api_endpoint ), |
845
|
|
|
$args, |
846
|
|
|
true |
847
|
|
|
); |
848
|
|
|
|
849
|
|
|
// Make sure the response is valid and does not contain any Jetpack errors. |
850
|
|
|
$registration_details = $this->validate_remote_register_response( $response ); |
851
|
|
|
|
852
|
|
|
if ( is_wp_error( $registration_details ) ) { |
853
|
|
|
return $registration_details; |
854
|
|
|
} elseif ( ! $registration_details ) { |
855
|
|
|
return new \WP_Error( |
856
|
|
|
'unknown_error', |
|
|
|
|
857
|
|
|
'Unknown error registering your Jetpack site.', |
858
|
|
|
wp_remote_retrieve_response_code( $response ) |
859
|
|
|
); |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) { |
863
|
|
|
return new \WP_Error( |
864
|
|
|
'jetpack_secret', |
|
|
|
|
865
|
|
|
'Unable to validate registration of your Jetpack site.', |
866
|
|
|
wp_remote_retrieve_response_code( $response ) |
867
|
|
|
); |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
if ( isset( $registration_details->jetpack_public ) ) { |
871
|
|
|
$jetpack_public = (int) $registration_details->jetpack_public; |
872
|
|
|
} else { |
873
|
|
|
$jetpack_public = false; |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
\Jetpack_Options::update_options( |
877
|
|
|
array( |
878
|
|
|
'id' => (int) $registration_details->jetpack_id, |
879
|
|
|
'blog_token' => (string) $registration_details->jetpack_secret, |
880
|
|
|
'public' => $jetpack_public, |
881
|
|
|
) |
882
|
|
|
); |
883
|
|
|
|
884
|
|
|
/** |
885
|
|
|
* Fires when a site is registered on WordPress.com. |
886
|
|
|
* |
887
|
|
|
* @since 3.7.0 |
888
|
|
|
* |
889
|
|
|
* @param int $json->jetpack_id Jetpack Blog ID. |
890
|
|
|
* @param string $json->jetpack_secret Jetpack Blog Token. |
891
|
|
|
* @param int|bool $jetpack_public Is the site public. |
892
|
|
|
*/ |
893
|
|
|
do_action( |
894
|
|
|
'jetpack_site_registered', |
895
|
|
|
$registration_details->jetpack_id, |
896
|
|
|
$registration_details->jetpack_secret, |
897
|
|
|
$jetpack_public |
898
|
|
|
); |
899
|
|
|
|
900
|
|
|
if ( isset( $registration_details->token ) ) { |
901
|
|
|
/** |
902
|
|
|
* Fires when a user token is sent along with the registration data. |
903
|
|
|
* |
904
|
|
|
* @since 7.6.0 |
905
|
|
|
* |
906
|
|
|
* @param object $token the administrator token for the newly registered site. |
907
|
|
|
*/ |
908
|
|
|
do_action( 'jetpack_site_registered_user_token', $registration_details->token ); |
909
|
|
|
} |
910
|
|
|
|
911
|
|
|
return true; |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
/** |
915
|
|
|
* Takes the response from the Jetpack register new site endpoint and |
916
|
|
|
* verifies it worked properly. |
917
|
|
|
* |
918
|
|
|
* @since 2.6 |
919
|
|
|
* |
920
|
|
|
* @param Mixed $response the response object, or the error object. |
921
|
|
|
* @return string|WP_Error A JSON object on success or Jetpack_Error on failures |
922
|
|
|
**/ |
923
|
|
|
protected function validate_remote_register_response( $response ) { |
924
|
|
|
if ( is_wp_error( $response ) ) { |
925
|
|
|
return new \WP_Error( |
926
|
|
|
'register_http_request_failed', |
|
|
|
|
927
|
|
|
$response->get_error_message() |
928
|
|
|
); |
929
|
|
|
} |
930
|
|
|
|
931
|
|
|
$code = wp_remote_retrieve_response_code( $response ); |
932
|
|
|
$entity = wp_remote_retrieve_body( $response ); |
933
|
|
|
|
934
|
|
|
if ( $entity ) { |
935
|
|
|
$registration_response = json_decode( $entity ); |
936
|
|
|
} else { |
937
|
|
|
$registration_response = false; |
938
|
|
|
} |
939
|
|
|
|
940
|
|
|
$code_type = intval( $code / 100 ); |
941
|
|
|
if ( 5 === $code_type ) { |
942
|
|
|
return new \WP_Error( 'wpcom_5??', $code ); |
|
|
|
|
943
|
|
|
} elseif ( 408 === $code ) { |
944
|
|
|
return new \WP_Error( 'wpcom_408', $code ); |
|
|
|
|
945
|
|
|
} elseif ( ! empty( $registration_response->error ) ) { |
946
|
|
|
if ( |
947
|
|
|
'xml_rpc-32700' === $registration_response->error |
948
|
|
|
&& ! function_exists( 'xml_parser_create' ) |
949
|
|
|
) { |
950
|
|
|
$error_description = __( "PHP's XML extension is not available. Jetpack requires the XML extension to communicate with WordPress.com. Please contact your hosting provider to enable PHP's XML extension.", 'jetpack' ); |
951
|
|
|
} else { |
952
|
|
|
$error_description = isset( $registration_response->error_description ) |
953
|
|
|
? (string) $registration_response->error_description |
954
|
|
|
: ''; |
955
|
|
|
} |
956
|
|
|
|
957
|
|
|
return new \WP_Error( |
958
|
|
|
(string) $registration_response->error, |
|
|
|
|
959
|
|
|
$error_description, |
960
|
|
|
$code |
961
|
|
|
); |
962
|
|
|
} elseif ( 200 !== $code ) { |
963
|
|
|
return new \WP_Error( 'wpcom_bad_response', $code ); |
|
|
|
|
964
|
|
|
} |
965
|
|
|
|
966
|
|
|
// Jetpack ID error block. |
967
|
|
|
if ( empty( $registration_response->jetpack_id ) ) { |
968
|
|
|
return new \WP_Error( |
969
|
|
|
'jetpack_id', |
|
|
|
|
970
|
|
|
/* translators: %s is an error message string */ |
971
|
|
|
sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
972
|
|
|
$entity |
973
|
|
|
); |
974
|
|
|
} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) { |
975
|
|
|
return new \WP_Error( |
976
|
|
|
'jetpack_id', |
|
|
|
|
977
|
|
|
/* translators: %s is an error message string */ |
978
|
|
|
sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
979
|
|
|
$entity |
980
|
|
|
); |
981
|
|
View Code Duplication |
} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) { |
982
|
|
|
return new \WP_Error( |
983
|
|
|
'jetpack_id', |
|
|
|
|
984
|
|
|
/* translators: %s is an error message string */ |
985
|
|
|
sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
986
|
|
|
$entity |
987
|
|
|
); |
988
|
|
|
} |
989
|
|
|
|
990
|
|
|
return $registration_response; |
991
|
|
|
} |
992
|
|
|
|
993
|
|
|
/** |
994
|
|
|
* Adds a used nonce to a list of known nonces. |
995
|
|
|
* |
996
|
|
|
* @param int $timestamp the current request timestamp. |
997
|
|
|
* @param string $nonce the nonce value. |
998
|
|
|
* @return bool whether the nonce is unique or not. |
999
|
|
|
*/ |
1000
|
|
|
public function add_nonce( $timestamp, $nonce ) { |
1001
|
|
|
global $wpdb; |
1002
|
|
|
static $nonces_used_this_request = array(); |
1003
|
|
|
|
1004
|
|
|
if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { |
1005
|
|
|
return $nonces_used_this_request[ "$timestamp:$nonce" ]; |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce. |
1009
|
|
|
$timestamp = (int) $timestamp; |
1010
|
|
|
$nonce = esc_sql( $nonce ); |
1011
|
|
|
|
1012
|
|
|
// Raw query so we can avoid races: add_option will also update. |
1013
|
|
|
$show_errors = $wpdb->show_errors( false ); |
1014
|
|
|
|
1015
|
|
|
$old_nonce = $wpdb->get_row( |
1016
|
|
|
$wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) |
1017
|
|
|
); |
1018
|
|
|
|
1019
|
|
|
if ( is_null( $old_nonce ) ) { |
1020
|
|
|
$return = $wpdb->query( |
1021
|
|
|
$wpdb->prepare( |
1022
|
|
|
"INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", |
1023
|
|
|
"jetpack_nonce_{$timestamp}_{$nonce}", |
1024
|
|
|
time(), |
1025
|
|
|
'no' |
1026
|
|
|
) |
1027
|
|
|
); |
1028
|
|
|
} else { |
1029
|
|
|
$return = false; |
1030
|
|
|
} |
1031
|
|
|
|
1032
|
|
|
$wpdb->show_errors( $show_errors ); |
1033
|
|
|
|
1034
|
|
|
$nonces_used_this_request[ "$timestamp:$nonce" ] = $return; |
1035
|
|
|
|
1036
|
|
|
return $return; |
1037
|
|
|
} |
1038
|
|
|
|
1039
|
|
|
/** |
1040
|
|
|
* Cleans nonces that were saved when calling ::add_nonce. |
1041
|
|
|
* |
1042
|
|
|
* @todo Properly prepare the query before executing it. |
1043
|
|
|
* |
1044
|
|
|
* @param bool $all whether to clean even non-expired nonces. |
1045
|
|
|
*/ |
1046
|
|
|
public function clean_nonces( $all = false ) { |
1047
|
|
|
global $wpdb; |
1048
|
|
|
|
1049
|
|
|
$sql = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s"; |
1050
|
|
|
$sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' ); |
1051
|
|
|
|
1052
|
|
|
if ( true !== $all ) { |
1053
|
|
|
$sql .= ' AND CAST( `option_value` AS UNSIGNED ) < %d'; |
1054
|
|
|
$sql_args[] = time() - 3600; |
1055
|
|
|
} |
1056
|
|
|
|
1057
|
|
|
$sql .= ' ORDER BY `option_id` LIMIT 100'; |
1058
|
|
|
|
1059
|
|
|
$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
1060
|
|
|
|
1061
|
|
|
for ( $i = 0; $i < 1000; $i++ ) { |
1062
|
|
|
if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
1063
|
|
|
break; |
1064
|
|
|
} |
1065
|
|
|
} |
1066
|
|
|
} |
1067
|
|
|
|
1068
|
|
|
/** |
1069
|
|
|
* Builds the timeout limit for queries talking with the wpcom servers. |
1070
|
|
|
* |
1071
|
|
|
* Based on local php max_execution_time in php.ini |
1072
|
|
|
* |
1073
|
|
|
* @since 5.4 |
1074
|
|
|
* @return int |
1075
|
|
|
**/ |
1076
|
|
|
public function get_max_execution_time() { |
1077
|
|
|
$timeout = (int) ini_get( 'max_execution_time' ); |
1078
|
|
|
|
1079
|
|
|
// Ensure exec time set in php.ini. |
1080
|
|
|
if ( ! $timeout ) { |
1081
|
|
|
$timeout = 30; |
1082
|
|
|
} |
1083
|
|
|
return $timeout; |
1084
|
|
|
} |
1085
|
|
|
|
1086
|
|
|
/** |
1087
|
|
|
* Sets a minimum request timeout, and returns the current timeout |
1088
|
|
|
* |
1089
|
|
|
* @since 5.4 |
1090
|
|
|
* @param Integer $min_timeout the minimum timeout value. |
1091
|
|
|
**/ |
1092
|
|
View Code Duplication |
public function set_min_time_limit( $min_timeout ) { |
1093
|
|
|
$timeout = $this->get_max_execution_time(); |
1094
|
|
|
if ( $timeout < $min_timeout ) { |
1095
|
|
|
$timeout = $min_timeout; |
1096
|
|
|
set_time_limit( $timeout ); |
1097
|
|
|
} |
1098
|
|
|
return $timeout; |
1099
|
|
|
} |
1100
|
|
|
|
1101
|
|
|
/** |
1102
|
|
|
* Get our assumed site creation date. |
1103
|
|
|
* Calculated based on the earlier date of either: |
1104
|
|
|
* - Earliest admin user registration date. |
1105
|
|
|
* - Earliest date of post of any post type. |
1106
|
|
|
* |
1107
|
|
|
* @since 7.2.0 |
1108
|
|
|
* |
1109
|
|
|
* @return string Assumed site creation date and time. |
1110
|
|
|
*/ |
1111
|
|
|
public function get_assumed_site_creation_date() { |
1112
|
|
|
$cached_date = get_transient( 'jetpack_assumed_site_creation_date' ); |
1113
|
|
|
if ( ! empty( $cached_date ) ) { |
1114
|
|
|
return $cached_date; |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
$earliest_registered_users = get_users( |
1118
|
|
|
array( |
1119
|
|
|
'role' => 'administrator', |
1120
|
|
|
'orderby' => 'user_registered', |
1121
|
|
|
'order' => 'ASC', |
1122
|
|
|
'fields' => array( 'user_registered' ), |
1123
|
|
|
'number' => 1, |
1124
|
|
|
) |
1125
|
|
|
); |
1126
|
|
|
$earliest_registration_date = $earliest_registered_users[0]->user_registered; |
1127
|
|
|
|
1128
|
|
|
$earliest_posts = get_posts( |
1129
|
|
|
array( |
1130
|
|
|
'posts_per_page' => 1, |
1131
|
|
|
'post_type' => 'any', |
1132
|
|
|
'post_status' => 'any', |
1133
|
|
|
'orderby' => 'date', |
1134
|
|
|
'order' => 'ASC', |
1135
|
|
|
) |
1136
|
|
|
); |
1137
|
|
|
|
1138
|
|
|
// If there are no posts at all, we'll count only on user registration date. |
1139
|
|
|
if ( $earliest_posts ) { |
1140
|
|
|
$earliest_post_date = $earliest_posts[0]->post_date; |
1141
|
|
|
} else { |
1142
|
|
|
$earliest_post_date = PHP_INT_MAX; |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
$assumed_date = min( $earliest_registration_date, $earliest_post_date ); |
1146
|
|
|
set_transient( 'jetpack_assumed_site_creation_date', $assumed_date ); |
1147
|
|
|
|
1148
|
|
|
return $assumed_date; |
1149
|
|
|
} |
1150
|
|
|
|
1151
|
|
|
/** |
1152
|
|
|
* Adds the activation source string as a parameter to passed arguments. |
1153
|
|
|
* |
1154
|
|
|
* @todo Refactor to use rawurlencode() instead of urlencode(). |
1155
|
|
|
* |
1156
|
|
|
* @param Array $args arguments that need to have the source added. |
1157
|
|
|
* @return Array $amended arguments. |
1158
|
|
|
*/ |
1159
|
|
View Code Duplication |
public static function apply_activation_source_to_args( $args ) { |
1160
|
|
|
list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' ); |
1161
|
|
|
|
1162
|
|
|
if ( $activation_source_name ) { |
1163
|
|
|
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode |
1164
|
|
|
$args['_as'] = urlencode( $activation_source_name ); |
1165
|
|
|
} |
1166
|
|
|
|
1167
|
|
|
if ( $activation_source_keyword ) { |
1168
|
|
|
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode |
1169
|
|
|
$args['_ak'] = urlencode( $activation_source_keyword ); |
1170
|
|
|
} |
1171
|
|
|
|
1172
|
|
|
return $args; |
1173
|
|
|
} |
1174
|
|
|
|
1175
|
|
|
/** |
1176
|
|
|
* Returns the callable that would be used to generate secrets. |
1177
|
|
|
* |
1178
|
|
|
* @return Callable a function that returns a secure string to be used as a secret. |
1179
|
|
|
*/ |
1180
|
|
|
protected function get_secret_callable() { |
1181
|
|
|
if ( ! isset( $this->secret_callable ) ) { |
1182
|
|
|
/** |
1183
|
|
|
* Allows modification of the callable that is used to generate connection secrets. |
1184
|
|
|
* |
1185
|
|
|
* @param Callable a function or method that returns a secret string. |
1186
|
|
|
*/ |
1187
|
|
|
$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', array( $this, 'secret_callable_method' ) ); |
1188
|
|
|
} |
1189
|
|
|
|
1190
|
|
|
return $this->secret_callable; |
1191
|
|
|
} |
1192
|
|
|
|
1193
|
|
|
/** |
1194
|
|
|
* Runs the wp_generate_password function with the required parameters. This is the |
1195
|
|
|
* default implementation of the secret callable, can be overridden using the |
1196
|
|
|
* jetpack_connection_secret_generator filter. |
1197
|
|
|
* |
1198
|
|
|
* @return String $secret value. |
1199
|
|
|
*/ |
1200
|
|
|
private function secret_callable_method() { |
1201
|
|
|
return wp_generate_password( 32, false ); |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
/** |
1205
|
|
|
* Generates two secret tokens and the end of life timestamp for them. |
1206
|
|
|
* |
1207
|
|
|
* @param String $action The action name. |
1208
|
|
|
* @param Integer $user_id The user identifier. |
|
|
|
|
1209
|
|
|
* @param Integer $exp Expiration time in seconds. |
1210
|
|
|
*/ |
1211
|
|
|
public function generate_secrets( $action, $user_id = false, $exp = 600 ) { |
1212
|
|
|
if ( false === $user_id ) { |
1213
|
|
|
$user_id = get_current_user_id(); |
1214
|
|
|
} |
1215
|
|
|
|
1216
|
|
|
$callable = $this->get_secret_callable(); |
1217
|
|
|
|
1218
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1219
|
|
|
self::SECRETS_OPTION_NAME, |
1220
|
|
|
array() |
1221
|
|
|
); |
1222
|
|
|
|
1223
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1224
|
|
|
|
1225
|
|
|
if ( |
1226
|
|
|
isset( $secrets[ $secret_name ] ) && |
1227
|
|
|
$secrets[ $secret_name ]['exp'] > time() |
1228
|
|
|
) { |
1229
|
|
|
return $secrets[ $secret_name ]; |
1230
|
|
|
} |
1231
|
|
|
|
1232
|
|
|
$secret_value = array( |
1233
|
|
|
'secret_1' => call_user_func( $callable ), |
1234
|
|
|
'secret_2' => call_user_func( $callable ), |
1235
|
|
|
'exp' => time() + $exp, |
1236
|
|
|
); |
1237
|
|
|
|
1238
|
|
|
$secrets[ $secret_name ] = $secret_value; |
1239
|
|
|
|
1240
|
|
|
\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); |
1241
|
|
|
return $secrets[ $secret_name ]; |
1242
|
|
|
} |
1243
|
|
|
|
1244
|
|
|
/** |
1245
|
|
|
* Returns two secret tokens and the end of life timestamp for them. |
1246
|
|
|
* |
1247
|
|
|
* @param String $action The action name. |
1248
|
|
|
* @param Integer $user_id The user identifier. |
1249
|
|
|
* @return string|array an array of secrets or an error string. |
1250
|
|
|
*/ |
1251
|
|
|
public function get_secrets( $action, $user_id ) { |
1252
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1253
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1254
|
|
|
self::SECRETS_OPTION_NAME, |
1255
|
|
|
array() |
1256
|
|
|
); |
1257
|
|
|
|
1258
|
|
|
if ( ! isset( $secrets[ $secret_name ] ) ) { |
1259
|
|
|
return self::SECRETS_MISSING; |
1260
|
|
|
} |
1261
|
|
|
|
1262
|
|
|
if ( $secrets[ $secret_name ]['exp'] < time() ) { |
1263
|
|
|
$this->delete_secrets( $action, $user_id ); |
1264
|
|
|
return self::SECRETS_EXPIRED; |
1265
|
|
|
} |
1266
|
|
|
|
1267
|
|
|
return $secrets[ $secret_name ]; |
1268
|
|
|
} |
1269
|
|
|
|
1270
|
|
|
/** |
1271
|
|
|
* Deletes secret tokens in case they, for example, have expired. |
1272
|
|
|
* |
1273
|
|
|
* @param String $action The action name. |
1274
|
|
|
* @param Integer $user_id The user identifier. |
1275
|
|
|
*/ |
1276
|
|
|
public function delete_secrets( $action, $user_id ) { |
1277
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1278
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1279
|
|
|
self::SECRETS_OPTION_NAME, |
1280
|
|
|
array() |
1281
|
|
|
); |
1282
|
|
|
if ( isset( $secrets[ $secret_name ] ) ) { |
1283
|
|
|
unset( $secrets[ $secret_name ] ); |
1284
|
|
|
\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); |
1285
|
|
|
} |
1286
|
|
|
} |
1287
|
|
|
|
1288
|
|
|
/** |
1289
|
|
|
* Deletes all connection tokens and transients from the local Jetpack site. |
1290
|
|
|
*/ |
1291
|
|
|
public function delete_all_connection_tokens() { |
1292
|
|
|
\Jetpack_Options::delete_option( |
1293
|
|
|
array( |
1294
|
|
|
'blog_token', |
1295
|
|
|
'user_token', |
1296
|
|
|
'user_tokens', |
1297
|
|
|
'master_user', |
1298
|
|
|
'time_diff', |
1299
|
|
|
'fallback_no_verify_ssl_certs', |
1300
|
|
|
) |
1301
|
|
|
); |
1302
|
|
|
|
1303
|
|
|
\Jetpack_Options::delete_raw_option( 'jetpack_secrets' ); |
1304
|
|
|
|
1305
|
|
|
// Delete cached connected user data. |
1306
|
|
|
$transient_key = 'jetpack_connected_user_data_' . get_current_user_id(); |
1307
|
|
|
delete_transient( $transient_key ); |
1308
|
|
|
} |
1309
|
|
|
|
1310
|
|
|
/** |
1311
|
|
|
* Tells WordPress.com to disconnect the site and clear all tokens from cached site. |
1312
|
|
|
*/ |
1313
|
|
|
public function disconnect_site_wpcom() { |
1314
|
|
|
$xml = new \Jetpack_IXR_Client(); |
1315
|
|
|
$xml->query( 'jetpack.deregister', get_current_user_id() ); |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
/** |
1319
|
|
|
* Responds to a WordPress.com call to register the current site. |
1320
|
|
|
* Should be changed to protected. |
1321
|
|
|
* |
1322
|
|
|
* @param array $registration_data Array of [ secret_1, user_id ]. |
1323
|
|
|
*/ |
1324
|
|
|
public function handle_registration( array $registration_data ) { |
1325
|
|
|
list( $registration_secret_1, $registration_user_id ) = $registration_data; |
1326
|
|
|
if ( empty( $registration_user_id ) ) { |
1327
|
|
|
return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 ); |
|
|
|
|
1328
|
|
|
} |
1329
|
|
|
|
1330
|
|
|
return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id ); |
1331
|
|
|
} |
1332
|
|
|
|
1333
|
|
|
/** |
1334
|
|
|
* Verify a Previously Generated Secret. |
1335
|
|
|
* |
1336
|
|
|
* @param string $action The type of secret to verify. |
1337
|
|
|
* @param string $secret_1 The secret string to compare to what is stored. |
1338
|
|
|
* @param int $user_id The user ID of the owner of the secret. |
1339
|
|
|
* @return \WP_Error|string WP_Error on failure, secret_2 on success. |
1340
|
|
|
*/ |
1341
|
|
|
public function verify_secrets( $action, $secret_1, $user_id ) { |
1342
|
|
|
$allowed_actions = array( 'register', 'authorize', 'publicize' ); |
1343
|
|
|
if ( ! in_array( $action, $allowed_actions, true ) ) { |
1344
|
|
|
return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 ); |
|
|
|
|
1345
|
|
|
} |
1346
|
|
|
|
1347
|
|
|
$user = get_user_by( 'id', $user_id ); |
1348
|
|
|
|
1349
|
|
|
/** |
1350
|
|
|
* We've begun verifying the previously generated secret. |
1351
|
|
|
* |
1352
|
|
|
* @since 7.5.0 |
1353
|
|
|
* |
1354
|
|
|
* @param string $action The type of secret to verify. |
1355
|
|
|
* @param \WP_User $user The user object. |
1356
|
|
|
*/ |
1357
|
|
|
do_action( 'jetpack_verify_secrets_begin', $action, $user ); |
1358
|
|
|
|
1359
|
|
|
$return_error = function( \WP_Error $error ) use ( $action, $user ) { |
1360
|
|
|
/** |
1361
|
|
|
* Verifying of the previously generated secret has failed. |
1362
|
|
|
* |
1363
|
|
|
* @since 7.5.0 |
1364
|
|
|
* |
1365
|
|
|
* @param string $action The type of secret to verify. |
1366
|
|
|
* @param \WP_User $user The user object. |
1367
|
|
|
* @param \WP_Error $error The error object. |
1368
|
|
|
*/ |
1369
|
|
|
do_action( 'jetpack_verify_secrets_fail', $action, $user, $error ); |
1370
|
|
|
|
1371
|
|
|
return $error; |
1372
|
|
|
}; |
1373
|
|
|
|
1374
|
|
|
$stored_secrets = $this->get_secrets( $action, $user_id ); |
1375
|
|
|
$this->delete_secrets( $action, $user_id ); |
1376
|
|
|
|
1377
|
|
|
$error = null; |
1378
|
|
|
if ( empty( $secret_1 ) ) { |
1379
|
|
|
$error = $return_error( |
1380
|
|
|
new \WP_Error( |
1381
|
|
|
'verify_secret_1_missing', |
|
|
|
|
1382
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1383
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ), |
1384
|
|
|
400 |
1385
|
|
|
) |
1386
|
|
|
); |
1387
|
|
|
} elseif ( ! is_string( $secret_1 ) ) { |
1388
|
|
|
$error = $return_error( |
1389
|
|
|
new \WP_Error( |
1390
|
|
|
'verify_secret_1_malformed', |
|
|
|
|
1391
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1392
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ), |
1393
|
|
|
400 |
1394
|
|
|
) |
1395
|
|
|
); |
1396
|
|
|
} elseif ( empty( $user_id ) ) { |
1397
|
|
|
// $user_id is passed around during registration as "state". |
1398
|
|
|
$error = $return_error( |
1399
|
|
|
new \WP_Error( |
1400
|
|
|
'state_missing', |
|
|
|
|
1401
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1402
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ), |
1403
|
|
|
400 |
1404
|
|
|
) |
1405
|
|
|
); |
1406
|
|
|
} elseif ( ! ctype_digit( (string) $user_id ) ) { |
1407
|
|
|
$error = $return_error( |
1408
|
|
|
new \WP_Error( |
1409
|
|
|
'state_malformed', |
|
|
|
|
1410
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1411
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ), |
1412
|
|
|
400 |
1413
|
|
|
) |
1414
|
|
|
); |
1415
|
|
|
} elseif ( self::SECRETS_MISSING === $stored_secrets ) { |
1416
|
|
|
$error = $return_error( |
1417
|
|
|
new \WP_Error( |
1418
|
|
|
'verify_secrets_missing', |
|
|
|
|
1419
|
|
|
__( 'Verification secrets not found', 'jetpack' ), |
1420
|
|
|
400 |
1421
|
|
|
) |
1422
|
|
|
); |
1423
|
|
|
} elseif ( self::SECRETS_EXPIRED === $stored_secrets ) { |
1424
|
|
|
$error = $return_error( |
1425
|
|
|
new \WP_Error( |
1426
|
|
|
'verify_secrets_expired', |
|
|
|
|
1427
|
|
|
__( 'Verification took too long', 'jetpack' ), |
1428
|
|
|
400 |
1429
|
|
|
) |
1430
|
|
|
); |
1431
|
|
|
} elseif ( ! $stored_secrets ) { |
1432
|
|
|
$error = $return_error( |
1433
|
|
|
new \WP_Error( |
1434
|
|
|
'verify_secrets_empty', |
|
|
|
|
1435
|
|
|
__( 'Verification secrets are empty', 'jetpack' ), |
1436
|
|
|
400 |
1437
|
|
|
) |
1438
|
|
|
); |
1439
|
|
|
} elseif ( is_wp_error( $stored_secrets ) ) { |
1440
|
|
|
$stored_secrets->add_data( 400 ); |
|
|
|
|
1441
|
|
|
$error = $return_error( $stored_secrets ); |
1442
|
|
|
} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) { |
1443
|
|
|
$error = $return_error( |
1444
|
|
|
new \WP_Error( |
1445
|
|
|
'verify_secrets_incomplete', |
|
|
|
|
1446
|
|
|
__( 'Verification secrets are incomplete', 'jetpack' ), |
1447
|
|
|
400 |
1448
|
|
|
) |
1449
|
|
|
); |
1450
|
|
|
} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { |
1451
|
|
|
$error = $return_error( |
1452
|
|
|
new \WP_Error( |
1453
|
|
|
'verify_secrets_mismatch', |
|
|
|
|
1454
|
|
|
__( 'Secret mismatch', 'jetpack' ), |
1455
|
|
|
400 |
1456
|
|
|
) |
1457
|
|
|
); |
1458
|
|
|
} |
1459
|
|
|
|
1460
|
|
|
// Something went wrong during the checks, returning the error. |
1461
|
|
|
if ( ! empty( $error ) ) { |
1462
|
|
|
return $error; |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
|
|
/** |
1466
|
|
|
* We've succeeded at verifying the previously generated secret. |
1467
|
|
|
* |
1468
|
|
|
* @since 7.5.0 |
1469
|
|
|
* |
1470
|
|
|
* @param string $action The type of secret to verify. |
1471
|
|
|
* @param \WP_User $user The user object. |
1472
|
|
|
*/ |
1473
|
|
|
do_action( 'jetpack_verify_secrets_success', $action, $user ); |
1474
|
|
|
|
1475
|
|
|
return $stored_secrets['secret_2']; |
1476
|
|
|
} |
1477
|
|
|
|
1478
|
|
|
/** |
1479
|
|
|
* Responds to a WordPress.com call to authorize the current user. |
1480
|
|
|
* Should be changed to protected. |
1481
|
|
|
*/ |
1482
|
|
|
public function handle_authorization() { |
1483
|
|
|
|
1484
|
|
|
} |
1485
|
|
|
|
1486
|
|
|
/** |
1487
|
|
|
* Obtains the auth token. |
1488
|
|
|
* |
1489
|
|
|
* @param array $data The request data. |
1490
|
|
|
* @return object|\WP_Error Returns the auth token on success. |
1491
|
|
|
* Returns a \WP_Error on failure. |
1492
|
|
|
*/ |
1493
|
|
|
public function get_token( $data ) { |
1494
|
|
|
$roles = new Roles(); |
1495
|
|
|
$role = $roles->translate_current_user_to_role(); |
1496
|
|
|
|
1497
|
|
|
if ( ! $role ) { |
1498
|
|
|
return new \WP_Error( 'role', __( 'An administrator for this blog must set up the Jetpack connection.', 'jetpack' ) ); |
|
|
|
|
1499
|
|
|
} |
1500
|
|
|
|
1501
|
|
|
$client_secret = $this->get_access_token(); |
1502
|
|
|
if ( ! $client_secret ) { |
1503
|
|
|
return new \WP_Error( 'client_secret', __( 'You need to register your Jetpack before connecting it.', 'jetpack' ) ); |
|
|
|
|
1504
|
|
|
} |
1505
|
|
|
|
1506
|
|
|
/** |
1507
|
|
|
* Filter the URL of the first time the user gets redirected back to your site for connection |
1508
|
|
|
* data processing. |
1509
|
|
|
* |
1510
|
|
|
* @since 8.0.0 |
1511
|
|
|
* |
1512
|
|
|
* @param string $redirect_url Defaults to the site admin URL. |
1513
|
|
|
*/ |
1514
|
|
|
$processing_url = apply_filters( 'jetpack_token_processing_url', admin_url( 'admin.php' ) ); |
1515
|
|
|
|
1516
|
|
|
$redirect = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : ''; |
1517
|
|
|
|
1518
|
|
|
/** |
1519
|
|
|
* Filter the URL to redirect the user back to when the authentication process |
1520
|
|
|
* is complete. |
1521
|
|
|
* |
1522
|
|
|
* @since 8.0.0 |
1523
|
|
|
* |
1524
|
|
|
* @param string $redirect_url Defaults to the site URL. |
1525
|
|
|
*/ |
1526
|
|
|
$redirect = apply_filters( 'jetpack_token_redirect_url', $redirect ); |
1527
|
|
|
|
1528
|
|
|
$redirect_uri = ( 'calypso' === $data['auth_type'] ) |
1529
|
|
|
? $data['redirect_uri'] |
1530
|
|
|
: add_query_arg( |
1531
|
|
|
array( |
1532
|
|
|
'action' => 'authorize', |
1533
|
|
|
'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), |
1534
|
|
|
'redirect' => $redirect ? rawurlencode( $redirect ) : false, |
1535
|
|
|
), |
1536
|
|
|
esc_url( $processing_url ) |
1537
|
|
|
); |
1538
|
|
|
|
1539
|
|
|
/** |
1540
|
|
|
* Filters the token request data. |
1541
|
|
|
* |
1542
|
|
|
* @since 8.0.0 |
1543
|
|
|
* |
1544
|
|
|
* @param Array $request_data request data. |
1545
|
|
|
*/ |
1546
|
|
|
$body = apply_filters( |
1547
|
|
|
'jetpack_token_request_body', |
1548
|
|
|
array( |
1549
|
|
|
'client_id' => \Jetpack_Options::get_option( 'id' ), |
1550
|
|
|
'client_secret' => $client_secret->secret, |
1551
|
|
|
'grant_type' => 'authorization_code', |
1552
|
|
|
'code' => $data['code'], |
1553
|
|
|
'redirect_uri' => $redirect_uri, |
1554
|
|
|
) |
1555
|
|
|
); |
1556
|
|
|
|
1557
|
|
|
$args = array( |
1558
|
|
|
'method' => 'POST', |
1559
|
|
|
'body' => $body, |
1560
|
|
|
'headers' => array( |
1561
|
|
|
'Accept' => 'application/json', |
1562
|
|
|
), |
1563
|
|
|
); |
1564
|
|
|
|
1565
|
|
|
$response = Client::_wp_remote_request( Utils::fix_url_for_bad_hosts( $this->api_url( 'token' ) ), $args ); |
1566
|
|
|
|
1567
|
|
|
if ( is_wp_error( $response ) ) { |
1568
|
|
|
return new \WP_Error( 'token_http_request_failed', $response->get_error_message() ); |
|
|
|
|
1569
|
|
|
} |
1570
|
|
|
|
1571
|
|
|
$code = wp_remote_retrieve_response_code( $response ); |
1572
|
|
|
$entity = wp_remote_retrieve_body( $response ); |
1573
|
|
|
|
1574
|
|
|
if ( $entity ) { |
1575
|
|
|
$json = json_decode( $entity ); |
1576
|
|
|
} else { |
1577
|
|
|
$json = false; |
1578
|
|
|
} |
1579
|
|
|
|
1580
|
|
|
if ( 200 !== $code || ! empty( $json->error ) ) { |
1581
|
|
|
if ( empty( $json->error ) ) { |
1582
|
|
|
return new \WP_Error( 'unknown', '', $code ); |
|
|
|
|
1583
|
|
|
} |
1584
|
|
|
|
1585
|
|
|
/* translators: Error description string. */ |
1586
|
|
|
$error_description = isset( $json->error_description ) ? sprintf( __( 'Error Details: %s', 'jetpack' ), (string) $json->error_description ) : ''; |
1587
|
|
|
|
1588
|
|
|
return new \WP_Error( (string) $json->error, $error_description, $code ); |
|
|
|
|
1589
|
|
|
} |
1590
|
|
|
|
1591
|
|
|
if ( empty( $json->access_token ) || ! is_scalar( $json->access_token ) ) { |
1592
|
|
|
return new \WP_Error( 'access_token', '', $code ); |
|
|
|
|
1593
|
|
|
} |
1594
|
|
|
|
1595
|
|
|
if ( empty( $json->token_type ) || 'X_JETPACK' !== strtoupper( $json->token_type ) ) { |
1596
|
|
|
return new \WP_Error( 'token_type', '', $code ); |
|
|
|
|
1597
|
|
|
} |
1598
|
|
|
|
1599
|
|
|
if ( empty( $json->scope ) ) { |
1600
|
|
|
return new \WP_Error( 'scope', 'No Scope', $code ); |
|
|
|
|
1601
|
|
|
} |
1602
|
|
|
|
1603
|
|
|
@list( $role, $hmac ) = explode( ':', $json->scope ); |
|
|
|
|
1604
|
|
|
if ( empty( $role ) || empty( $hmac ) ) { |
1605
|
|
|
return new \WP_Error( 'scope', 'Malformed Scope', $code ); |
|
|
|
|
1606
|
|
|
} |
1607
|
|
|
|
1608
|
|
|
if ( $this->sign_role( $role ) !== $json->scope ) { |
1609
|
|
|
return new \WP_Error( 'scope', 'Invalid Scope', $code ); |
|
|
|
|
1610
|
|
|
} |
1611
|
|
|
|
1612
|
|
|
$cap = $roles->translate_role_to_cap( $role ); |
1613
|
|
|
if ( ! $cap ) { |
1614
|
|
|
return new \WP_Error( 'scope', 'No Cap', $code ); |
|
|
|
|
1615
|
|
|
} |
1616
|
|
|
|
1617
|
|
|
if ( ! current_user_can( $cap ) ) { |
1618
|
|
|
return new \WP_Error( 'scope', 'current_user_cannot', $code ); |
|
|
|
|
1619
|
|
|
} |
1620
|
|
|
|
1621
|
|
|
/** |
1622
|
|
|
* Fires after user has successfully received an auth token. |
1623
|
|
|
* |
1624
|
|
|
* @since 3.9.0 |
1625
|
|
|
*/ |
1626
|
|
|
do_action( 'jetpack_user_authorized' ); |
1627
|
|
|
|
1628
|
|
|
return (string) $json->access_token; |
1629
|
|
|
} |
1630
|
|
|
|
1631
|
|
|
/** |
1632
|
|
|
* Builds a URL to the Jetpack connection auth page. |
1633
|
|
|
* |
1634
|
|
|
* @param WP_User $user (optional) defaults to the current logged in user. |
|
|
|
|
1635
|
|
|
* @param String $redirect (optional) a redirect URL to use instead of the default. |
|
|
|
|
1636
|
|
|
* @return string Connect URL. |
1637
|
|
|
*/ |
1638
|
|
|
public function get_authorization_url( $user = null, $redirect = null ) { |
1639
|
|
|
|
1640
|
|
|
if ( empty( $user ) ) { |
1641
|
|
|
$user = wp_get_current_user(); |
1642
|
|
|
} |
1643
|
|
|
|
1644
|
|
|
$roles = new Roles(); |
1645
|
|
|
$role = $roles->translate_user_to_role( $user ); |
1646
|
|
|
$signed_role = $this->sign_role( $role ); |
1647
|
|
|
|
1648
|
|
|
/** |
1649
|
|
|
* Filter the URL of the first time the user gets redirected back to your site for connection |
1650
|
|
|
* data processing. |
1651
|
|
|
* |
1652
|
|
|
* @since 8.0.0 |
1653
|
|
|
* |
1654
|
|
|
* @param string $redirect_url Defaults to the site admin URL. |
1655
|
|
|
*/ |
1656
|
|
|
$processing_url = apply_filters( 'jetpack_connect_processing_url', admin_url( 'admin.php' ) ); |
1657
|
|
|
|
1658
|
|
|
/** |
1659
|
|
|
* Filter the URL to redirect the user back to when the authorization process |
1660
|
|
|
* is complete. |
1661
|
|
|
* |
1662
|
|
|
* @since 8.0.0 |
1663
|
|
|
* |
1664
|
|
|
* @param string $redirect_url Defaults to the site URL. |
1665
|
|
|
*/ |
1666
|
|
|
$redirect = apply_filters( 'jetpack_connect_redirect_url', $redirect ); |
1667
|
|
|
|
1668
|
|
|
$secrets = $this->generate_secrets( 'authorize', $user->ID, 2 * HOUR_IN_SECONDS ); |
1669
|
|
|
|
1670
|
|
|
/** |
1671
|
|
|
* Filter the type of authorization. |
1672
|
|
|
* 'calypso' completes authorization on wordpress.com/jetpack/connect |
1673
|
|
|
* while 'jetpack' ( or any other value ) completes the authorization at jetpack.wordpress.com. |
1674
|
|
|
* |
1675
|
|
|
* @since 4.3.3 |
1676
|
|
|
* |
1677
|
|
|
* @param string $auth_type Defaults to 'calypso', can also be 'jetpack'. |
1678
|
|
|
*/ |
1679
|
|
|
$auth_type = apply_filters( 'jetpack_auth_type', 'calypso' ); |
1680
|
|
|
|
1681
|
|
|
/** |
1682
|
|
|
* Filters the user connection request data for additional property addition. |
1683
|
|
|
* |
1684
|
|
|
* @since 8.0.0 |
1685
|
|
|
* |
1686
|
|
|
* @param Array $request_data request data. |
1687
|
|
|
*/ |
1688
|
|
|
$body = apply_filters( |
1689
|
|
|
'jetpack_connect_request_body', |
1690
|
|
|
array( |
1691
|
|
|
'response_type' => 'code', |
1692
|
|
|
'client_id' => \Jetpack_Options::get_option( 'id' ), |
1693
|
|
|
'redirect_uri' => add_query_arg( |
1694
|
|
|
array( |
1695
|
|
|
'action' => 'authorize', |
1696
|
|
|
'_wpnonce' => wp_create_nonce( "jetpack-authorize_{$role}_{$redirect}" ), |
1697
|
|
|
'redirect' => rawurlencode( $redirect ), |
1698
|
|
|
), |
1699
|
|
|
esc_url( $processing_url ) |
1700
|
|
|
), |
1701
|
|
|
'state' => $user->ID, |
1702
|
|
|
'scope' => $signed_role, |
1703
|
|
|
'user_email' => $user->user_email, |
1704
|
|
|
'user_login' => $user->user_login, |
1705
|
|
|
'is_active' => $this->is_active(), |
1706
|
|
|
'jp_version' => Constants::get_constant( 'JETPACK__VERSION' ), |
1707
|
|
|
'auth_type' => $auth_type, |
1708
|
|
|
'secret' => $secrets['secret_1'], |
1709
|
|
|
'blogname' => get_option( 'blogname' ), |
1710
|
|
|
'site_url' => site_url(), |
1711
|
|
|
'home_url' => home_url(), |
1712
|
|
|
'site_icon' => get_site_icon_url(), |
1713
|
|
|
'site_lang' => get_locale(), |
1714
|
|
|
'site_created' => $this->get_assumed_site_creation_date(), |
1715
|
|
|
) |
1716
|
|
|
); |
1717
|
|
|
|
1718
|
|
|
$body = $this->apply_activation_source_to_args( urlencode_deep( $body ) ); |
1719
|
|
|
|
1720
|
|
|
$api_url = $this->api_url( 'authorize' ); |
1721
|
|
|
|
1722
|
|
|
return add_query_arg( $body, $api_url ); |
1723
|
|
|
} |
1724
|
|
|
|
1725
|
|
|
/** |
1726
|
|
|
* Authorizes the user by obtaining and storing the user token. |
1727
|
|
|
* |
1728
|
|
|
* @param array $data The request data. |
1729
|
|
|
* @return string|\WP_Error Returns a string on success. |
1730
|
|
|
* Returns a \WP_Error on failure. |
1731
|
|
|
*/ |
1732
|
|
|
public function authorize( $data = array() ) { |
1733
|
|
|
/** |
1734
|
|
|
* Action fired when user authorization starts. |
1735
|
|
|
* |
1736
|
|
|
* @since 8.0.0 |
1737
|
|
|
*/ |
1738
|
|
|
do_action( 'jetpack_authorize_starting' ); |
1739
|
|
|
|
1740
|
|
|
$roles = new Roles(); |
1741
|
|
|
$role = $roles->translate_current_user_to_role(); |
1742
|
|
|
|
1743
|
|
|
if ( ! $role ) { |
1744
|
|
|
return new \WP_Error( 'no_role', 'Invalid request.', 400 ); |
|
|
|
|
1745
|
|
|
} |
1746
|
|
|
|
1747
|
|
|
$cap = $roles->translate_role_to_cap( $role ); |
1748
|
|
|
if ( ! $cap ) { |
1749
|
|
|
return new \WP_Error( 'no_cap', 'Invalid request.', 400 ); |
|
|
|
|
1750
|
|
|
} |
1751
|
|
|
|
1752
|
|
|
if ( ! empty( $data['error'] ) ) { |
1753
|
|
|
return new \WP_Error( $data['error'], 'Error included in the request.', 400 ); |
|
|
|
|
1754
|
|
|
} |
1755
|
|
|
|
1756
|
|
|
if ( ! isset( $data['state'] ) ) { |
1757
|
|
|
return new \WP_Error( 'no_state', 'Request must include state.', 400 ); |
|
|
|
|
1758
|
|
|
} |
1759
|
|
|
|
1760
|
|
|
if ( ! ctype_digit( $data['state'] ) ) { |
1761
|
|
|
return new \WP_Error( $data['error'], 'State must be an integer.', 400 ); |
|
|
|
|
1762
|
|
|
} |
1763
|
|
|
|
1764
|
|
|
$current_user_id = get_current_user_id(); |
1765
|
|
|
if ( $current_user_id !== (int) $data['state'] ) { |
1766
|
|
|
return new \WP_Error( 'wrong_state', 'State does not match current user.', 400 ); |
|
|
|
|
1767
|
|
|
} |
1768
|
|
|
|
1769
|
|
|
if ( empty( $data['code'] ) ) { |
1770
|
|
|
return new \WP_Error( 'no_code', 'Request must include an authorization code.', 400 ); |
|
|
|
|
1771
|
|
|
} |
1772
|
|
|
|
1773
|
|
|
$token = $this->get_token( $data ); |
1774
|
|
|
|
1775
|
|
View Code Duplication |
if ( is_wp_error( $token ) ) { |
1776
|
|
|
$code = $token->get_error_code(); |
|
|
|
|
1777
|
|
|
if ( empty( $code ) ) { |
1778
|
|
|
$code = 'invalid_token'; |
1779
|
|
|
} |
1780
|
|
|
return new \WP_Error( $code, $token->get_error_message(), 400 ); |
|
|
|
|
1781
|
|
|
} |
1782
|
|
|
|
1783
|
|
|
if ( ! $token ) { |
1784
|
|
|
return new \WP_Error( 'no_token', 'Error generating token.', 400 ); |
|
|
|
|
1785
|
|
|
} |
1786
|
|
|
|
1787
|
|
|
$is_master_user = ! $this->is_active(); |
1788
|
|
|
|
1789
|
|
|
Utils::update_user_token( $current_user_id, sprintf( '%s.%d', $token, $current_user_id ), $is_master_user ); |
1790
|
|
|
|
1791
|
|
|
if ( ! $is_master_user ) { |
1792
|
|
|
/** |
1793
|
|
|
* Action fired when a secondary user has been authorized. |
1794
|
|
|
* |
1795
|
|
|
* @since 8.0.0 |
1796
|
|
|
*/ |
1797
|
|
|
do_action( 'jetpack_authorize_ending_linked' ); |
1798
|
|
|
return 'linked'; |
1799
|
|
|
} |
1800
|
|
|
|
1801
|
|
|
/** |
1802
|
|
|
* Action fired when the master user has been authorized. |
1803
|
|
|
* |
1804
|
|
|
* @since 8.0.0 |
1805
|
|
|
* |
1806
|
|
|
* @param array $data The request data. |
1807
|
|
|
*/ |
1808
|
|
|
do_action( 'jetpack_authorize_ending_authorized', $data ); |
1809
|
|
|
|
1810
|
|
|
return 'authorized'; |
1811
|
|
|
} |
1812
|
|
|
|
1813
|
|
|
/** |
1814
|
|
|
* Disconnects from the Jetpack servers. |
1815
|
|
|
* Forgets all connection details and tells the Jetpack servers to do the same. |
1816
|
|
|
*/ |
1817
|
|
|
public function disconnect_site() { |
1818
|
|
|
|
1819
|
|
|
} |
1820
|
|
|
|
1821
|
|
|
/** |
1822
|
|
|
* The Base64 Encoding of the SHA1 Hash of the Input. |
1823
|
|
|
* |
1824
|
|
|
* @param string $text The string to hash. |
1825
|
|
|
* @return string |
1826
|
|
|
*/ |
1827
|
|
|
public function sha1_base64( $text ) { |
1828
|
|
|
return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
1829
|
|
|
} |
1830
|
|
|
|
1831
|
|
|
/** |
1832
|
|
|
* This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase. |
1833
|
|
|
* |
1834
|
|
|
* @param string $domain The domain to check. |
1835
|
|
|
* |
1836
|
|
|
* @return bool|WP_Error |
1837
|
|
|
*/ |
1838
|
|
|
public function is_usable_domain( $domain ) { |
1839
|
|
|
|
1840
|
|
|
// If it's empty, just fail out. |
1841
|
|
|
if ( ! $domain ) { |
1842
|
|
|
return new \WP_Error( |
1843
|
|
|
'fail_domain_empty', |
|
|
|
|
1844
|
|
|
/* translators: %1$s is a domain name. */ |
1845
|
|
|
sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) |
1846
|
|
|
); |
1847
|
|
|
} |
1848
|
|
|
|
1849
|
|
|
/** |
1850
|
|
|
* Skips the usuable domain check when connecting a site. |
1851
|
|
|
* |
1852
|
|
|
* Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com |
1853
|
|
|
* |
1854
|
|
|
* @since 4.1.0 |
1855
|
|
|
* |
1856
|
|
|
* @param bool If the check should be skipped. Default false. |
1857
|
|
|
*/ |
1858
|
|
|
if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) { |
1859
|
|
|
return true; |
1860
|
|
|
} |
1861
|
|
|
|
1862
|
|
|
// None of the explicit localhosts. |
1863
|
|
|
$forbidden_domains = array( |
1864
|
|
|
'wordpress.com', |
1865
|
|
|
'localhost', |
1866
|
|
|
'localhost.localdomain', |
1867
|
|
|
'127.0.0.1', |
1868
|
|
|
'local.wordpress.test', // VVV pattern. |
1869
|
|
|
'local.wordpress-trunk.test', // VVV pattern. |
1870
|
|
|
'src.wordpress-develop.test', // VVV pattern. |
1871
|
|
|
'build.wordpress-develop.test', // VVV pattern. |
1872
|
|
|
); |
1873
|
|
View Code Duplication |
if ( in_array( $domain, $forbidden_domains, true ) ) { |
1874
|
|
|
return new \WP_Error( |
1875
|
|
|
'fail_domain_forbidden', |
|
|
|
|
1876
|
|
|
sprintf( |
1877
|
|
|
/* translators: %1$s is a domain name. */ |
1878
|
|
|
__( |
1879
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', |
1880
|
|
|
'jetpack' |
1881
|
|
|
), |
1882
|
|
|
$domain |
1883
|
|
|
) |
1884
|
|
|
); |
1885
|
|
|
} |
1886
|
|
|
|
1887
|
|
|
// No .test or .local domains. |
1888
|
|
View Code Duplication |
if ( preg_match( '#\.(test|local)$#i', $domain ) ) { |
1889
|
|
|
return new \WP_Error( |
1890
|
|
|
'fail_domain_tld', |
|
|
|
|
1891
|
|
|
sprintf( |
1892
|
|
|
/* translators: %1$s is a domain name. */ |
1893
|
|
|
__( |
1894
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', |
1895
|
|
|
'jetpack' |
1896
|
|
|
), |
1897
|
|
|
$domain |
1898
|
|
|
) |
1899
|
|
|
); |
1900
|
|
|
} |
1901
|
|
|
|
1902
|
|
|
// No WPCOM subdomains. |
1903
|
|
View Code Duplication |
if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) { |
1904
|
|
|
return new \WP_Error( |
1905
|
|
|
'fail_subdomain_wpcom', |
|
|
|
|
1906
|
|
|
sprintf( |
1907
|
|
|
/* translators: %1$s is a domain name. */ |
1908
|
|
|
__( |
1909
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', |
1910
|
|
|
'jetpack' |
1911
|
|
|
), |
1912
|
|
|
$domain |
1913
|
|
|
) |
1914
|
|
|
); |
1915
|
|
|
} |
1916
|
|
|
|
1917
|
|
|
// If PHP was compiled without support for the Filter module (very edge case). |
1918
|
|
|
if ( ! function_exists( 'filter_var' ) ) { |
1919
|
|
|
// Just pass back true for now, and let wpcom sort it out. |
1920
|
|
|
return true; |
1921
|
|
|
} |
1922
|
|
|
|
1923
|
|
|
return true; |
1924
|
|
|
} |
1925
|
|
|
|
1926
|
|
|
/** |
1927
|
|
|
* Gets the requested token. |
1928
|
|
|
* |
1929
|
|
|
* Tokens are one of two types: |
1930
|
|
|
* 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token, |
1931
|
|
|
* though some sites can have multiple "Special" Blog Tokens (see below). These tokens |
1932
|
|
|
* are not associated with a user account. They represent the site's connection with |
1933
|
|
|
* the Jetpack servers. |
1934
|
|
|
* 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token. |
1935
|
|
|
* |
1936
|
|
|
* All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the |
1937
|
|
|
* token, and $private is a secret that should never be displayed anywhere or sent |
1938
|
|
|
* over the network; it's used only for signing things. |
1939
|
|
|
* |
1940
|
|
|
* Blog Tokens can be "Normal" or "Special". |
1941
|
|
|
* * Normal: The result of a normal connection flow. They look like |
1942
|
|
|
* "{$random_string_1}.{$random_string_2}" |
1943
|
|
|
* That is, $token_key and $private are both random strings. |
1944
|
|
|
* Sites only have one Normal Blog Token. Normal Tokens are found in either |
1945
|
|
|
* Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN |
1946
|
|
|
* constant (rare). |
1947
|
|
|
* * Special: A connection token for sites that have gone through an alternative |
1948
|
|
|
* connection flow. They look like: |
1949
|
|
|
* ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}" |
1950
|
|
|
* That is, $private is a random string and $token_key has a special structure with |
1951
|
|
|
* lots of semicolons. |
1952
|
|
|
* Most sites have zero Special Blog Tokens. Special tokens are only found in the |
1953
|
|
|
* JETPACK_BLOG_TOKEN constant. |
1954
|
|
|
* |
1955
|
|
|
* In particular, note that Normal Blog Tokens never start with ";" and that |
1956
|
|
|
* Special Blog Tokens always do. |
1957
|
|
|
* |
1958
|
|
|
* When searching for a matching Blog Tokens, Blog Tokens are examined in the following |
1959
|
|
|
* order: |
1960
|
|
|
* 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant) |
1961
|
|
|
* 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' )) |
1962
|
|
|
* 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant) |
1963
|
|
|
* |
1964
|
|
|
* @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. |
1965
|
|
|
* @param string|false $token_key If provided, check that the token matches the provided input. |
1966
|
|
|
* @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. |
1967
|
|
|
* |
1968
|
|
|
* @return object|false |
1969
|
|
|
*/ |
1970
|
|
|
public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { |
1971
|
|
|
$possible_special_tokens = array(); |
1972
|
|
|
$possible_normal_tokens = array(); |
1973
|
|
|
$user_tokens = \Jetpack_Options::get_option( 'user_tokens' ); |
1974
|
|
|
|
1975
|
|
|
if ( $user_id ) { |
|
|
|
|
1976
|
|
|
if ( ! $user_tokens ) { |
1977
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' ); |
|
|
|
|
1978
|
|
|
} |
1979
|
|
|
if ( self::JETPACK_MASTER_USER === $user_id ) { |
1980
|
|
|
$user_id = \Jetpack_Options::get_option( 'master_user' ); |
1981
|
|
|
if ( ! $user_id ) { |
1982
|
|
|
return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' ); |
|
|
|
|
1983
|
|
|
} |
1984
|
|
|
} |
1985
|
|
|
if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) { |
1986
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) ); |
|
|
|
|
1987
|
|
|
} |
1988
|
|
|
$user_token_chunks = explode( '.', $user_tokens[ $user_id ] ); |
1989
|
|
View Code Duplication |
if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) { |
1990
|
|
|
return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) ); |
|
|
|
|
1991
|
|
|
} |
1992
|
|
View Code Duplication |
if ( $user_token_chunks[2] !== (string) $user_id ) { |
1993
|
|
|
return $suppress_errors ? false : new \WP_Error( 'user_id_mismatch', sprintf( 'Requesting user_id %d does not match token user_id %d', $user_id, $user_token_chunks[2] ) ); |
|
|
|
|
1994
|
|
|
} |
1995
|
|
|
$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}"; |
1996
|
|
|
} else { |
1997
|
|
|
$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' ); |
1998
|
|
|
if ( $stored_blog_token ) { |
1999
|
|
|
$possible_normal_tokens[] = $stored_blog_token; |
2000
|
|
|
} |
2001
|
|
|
|
2002
|
|
|
$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' ); |
2003
|
|
|
|
2004
|
|
|
if ( $defined_tokens_string ) { |
2005
|
|
|
$defined_tokens = explode( ',', $defined_tokens_string ); |
2006
|
|
|
foreach ( $defined_tokens as $defined_token ) { |
2007
|
|
|
if ( ';' === $defined_token[0] ) { |
2008
|
|
|
$possible_special_tokens[] = $defined_token; |
2009
|
|
|
} else { |
2010
|
|
|
$possible_normal_tokens[] = $defined_token; |
2011
|
|
|
} |
2012
|
|
|
} |
2013
|
|
|
} |
2014
|
|
|
} |
2015
|
|
|
|
2016
|
|
|
if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { |
2017
|
|
|
$possible_tokens = $possible_normal_tokens; |
2018
|
|
|
} else { |
2019
|
|
|
$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens ); |
2020
|
|
|
} |
2021
|
|
|
|
2022
|
|
|
if ( ! $possible_tokens ) { |
|
|
|
|
2023
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' ); |
|
|
|
|
2024
|
|
|
} |
2025
|
|
|
|
2026
|
|
|
$valid_token = false; |
2027
|
|
|
|
2028
|
|
|
if ( false === $token_key ) { |
2029
|
|
|
// Use first token. |
2030
|
|
|
$valid_token = $possible_tokens[0]; |
2031
|
|
|
} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { |
2032
|
|
|
// Use first normal token. |
2033
|
|
|
$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check. |
2034
|
|
|
} else { |
2035
|
|
|
// Use the token matching $token_key or false if none. |
2036
|
|
|
// Ensure we check the full key. |
2037
|
|
|
$token_check = rtrim( $token_key, '.' ) . '.'; |
2038
|
|
|
|
2039
|
|
|
foreach ( $possible_tokens as $possible_token ) { |
2040
|
|
|
if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) { |
2041
|
|
|
$valid_token = $possible_token; |
2042
|
|
|
break; |
2043
|
|
|
} |
2044
|
|
|
} |
2045
|
|
|
} |
2046
|
|
|
|
2047
|
|
|
if ( ! $valid_token ) { |
2048
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_valid_token' ); |
|
|
|
|
2049
|
|
|
} |
2050
|
|
|
|
2051
|
|
|
return (object) array( |
2052
|
|
|
'secret' => $valid_token, |
2053
|
|
|
'external_user_id' => (int) $user_id, |
2054
|
|
|
); |
2055
|
|
|
} |
2056
|
|
|
|
2057
|
|
|
/** |
2058
|
|
|
* In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths |
2059
|
|
|
* since it is passed by reference to various methods. |
2060
|
|
|
* Capture it here so we can verify the signature later. |
2061
|
|
|
* |
2062
|
|
|
* @param Array $methods an array of available XMLRPC methods. |
2063
|
|
|
* @return Array the same array, since this method doesn't add or remove anything. |
2064
|
|
|
*/ |
2065
|
|
|
public function xmlrpc_methods( $methods ) { |
2066
|
|
|
$this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA']; |
2067
|
|
|
return $methods; |
2068
|
|
|
} |
2069
|
|
|
|
2070
|
|
|
/** |
2071
|
|
|
* Resets the raw post data parameter for testing purposes. |
2072
|
|
|
*/ |
2073
|
|
|
public function reset_raw_post_data() { |
2074
|
|
|
$this->raw_post_data = null; |
2075
|
|
|
} |
2076
|
|
|
|
2077
|
|
|
/** |
2078
|
|
|
* Registering an additional method. |
2079
|
|
|
* |
2080
|
|
|
* @param Array $methods an array of available XMLRPC methods. |
2081
|
|
|
* @return Array the amended array in case the method is added. |
2082
|
|
|
*/ |
2083
|
|
|
public function public_xmlrpc_methods( $methods ) { |
2084
|
|
|
if ( array_key_exists( 'wp.getOptions', $methods ) ) { |
2085
|
|
|
$methods['wp.getOptions'] = array( $this, 'jetpack_get_options' ); |
2086
|
|
|
} |
2087
|
|
|
return $methods; |
2088
|
|
|
} |
2089
|
|
|
|
2090
|
|
|
/** |
2091
|
|
|
* Handles a getOptions XMLRPC method call. |
2092
|
|
|
* |
2093
|
|
|
* @param Array $args method call arguments. |
2094
|
|
|
* @return an amended XMLRPC server options array. |
2095
|
|
|
*/ |
2096
|
|
|
public function jetpack_get_options( $args ) { |
2097
|
|
|
global $wp_xmlrpc_server; |
2098
|
|
|
|
2099
|
|
|
$wp_xmlrpc_server->escape( $args ); |
2100
|
|
|
|
2101
|
|
|
$username = $args[1]; |
2102
|
|
|
$password = $args[2]; |
2103
|
|
|
|
2104
|
|
|
$user = $wp_xmlrpc_server->login( $username, $password ); |
2105
|
|
|
if ( ! $user ) { |
2106
|
|
|
return $wp_xmlrpc_server->error; |
2107
|
|
|
} |
2108
|
|
|
|
2109
|
|
|
$options = array(); |
2110
|
|
|
$user_data = $this->get_connected_user_data(); |
2111
|
|
|
if ( is_array( $user_data ) ) { |
2112
|
|
|
$options['jetpack_user_id'] = array( |
2113
|
|
|
'desc' => __( 'The WP.com user ID of the connected user', 'jetpack' ), |
2114
|
|
|
'readonly' => true, |
2115
|
|
|
'value' => $user_data['ID'], |
2116
|
|
|
); |
2117
|
|
|
$options['jetpack_user_login'] = array( |
2118
|
|
|
'desc' => __( 'The WP.com username of the connected user', 'jetpack' ), |
2119
|
|
|
'readonly' => true, |
2120
|
|
|
'value' => $user_data['login'], |
2121
|
|
|
); |
2122
|
|
|
$options['jetpack_user_email'] = array( |
2123
|
|
|
'desc' => __( 'The WP.com user email of the connected user', 'jetpack' ), |
2124
|
|
|
'readonly' => true, |
2125
|
|
|
'value' => $user_data['email'], |
2126
|
|
|
); |
2127
|
|
|
$options['jetpack_user_site_count'] = array( |
2128
|
|
|
'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack' ), |
2129
|
|
|
'readonly' => true, |
2130
|
|
|
'value' => $user_data['site_count'], |
2131
|
|
|
); |
2132
|
|
|
} |
2133
|
|
|
$wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options ); |
2134
|
|
|
$args = stripslashes_deep( $args ); |
2135
|
|
|
return $wp_xmlrpc_server->wp_getOptions( $args ); |
2136
|
|
|
} |
2137
|
|
|
|
2138
|
|
|
/** |
2139
|
|
|
* Adds Jetpack-specific options to the output of the XMLRPC options method. |
2140
|
|
|
* |
2141
|
|
|
* @param Array $options standard Core options. |
2142
|
|
|
* @return Array amended options. |
2143
|
|
|
*/ |
2144
|
|
|
public function xmlrpc_options( $options ) { |
2145
|
|
|
$jetpack_client_id = false; |
2146
|
|
|
if ( $this->is_active() ) { |
2147
|
|
|
$jetpack_client_id = \Jetpack_Options::get_option( 'id' ); |
2148
|
|
|
} |
2149
|
|
|
$options['jetpack_version'] = array( |
2150
|
|
|
'desc' => __( 'Jetpack Plugin Version', 'jetpack' ), |
2151
|
|
|
'readonly' => true, |
2152
|
|
|
'value' => Constants::get_constant( 'JETPACK__VERSION' ), |
2153
|
|
|
); |
2154
|
|
|
|
2155
|
|
|
$options['jetpack_client_id'] = array( |
2156
|
|
|
'desc' => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack' ), |
2157
|
|
|
'readonly' => true, |
2158
|
|
|
'value' => $jetpack_client_id, |
2159
|
|
|
); |
2160
|
|
|
return $options; |
2161
|
|
|
} |
2162
|
|
|
|
2163
|
|
|
/** |
2164
|
|
|
* Resets the saved authentication state in between testing requests. |
2165
|
|
|
*/ |
2166
|
|
|
public function reset_saved_auth_state() { |
2167
|
|
|
$this->xmlrpc_verification = null; |
2168
|
|
|
} |
2169
|
|
|
|
2170
|
|
|
/** |
2171
|
|
|
* Sign a user role with the master access token. |
2172
|
|
|
* If not specified, will default to the current user. |
2173
|
|
|
* |
2174
|
|
|
* @access public |
2175
|
|
|
* |
2176
|
|
|
* @param string $role User role. |
2177
|
|
|
* @param int $user_id ID of the user. |
|
|
|
|
2178
|
|
|
* @return string Signed user role. |
2179
|
|
|
*/ |
2180
|
|
|
public function sign_role( $role, $user_id = null ) { |
2181
|
|
|
if ( empty( $user_id ) ) { |
2182
|
|
|
$user_id = (int) get_current_user_id(); |
2183
|
|
|
} |
2184
|
|
|
|
2185
|
|
|
if ( ! $user_id ) { |
2186
|
|
|
return false; |
2187
|
|
|
} |
2188
|
|
|
|
2189
|
|
|
$token = $this->get_access_token(); |
2190
|
|
|
if ( ! $token || is_wp_error( $token ) ) { |
2191
|
|
|
return false; |
2192
|
|
|
} |
2193
|
|
|
|
2194
|
|
|
return $role . ':' . hash_hmac( 'md5', "{$role}|{$user_id}", $token->secret ); |
2195
|
|
|
} |
2196
|
|
|
} |
2197
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.