1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* The Jetpack Connection manager class file. |
4
|
|
|
* |
5
|
|
|
* @package jetpack-connection |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Automattic\Jetpack\Connection; |
9
|
|
|
|
10
|
|
|
use Automattic\Jetpack\Constants; |
11
|
|
|
use Automattic\Jetpack\Tracking; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* The Jetpack Connection Manager class that is used as a single gateway between WordPress.com |
15
|
|
|
* and Jetpack. |
16
|
|
|
*/ |
17
|
|
|
class Manager implements Manager_Interface { |
18
|
|
|
|
19
|
|
|
const SECRETS_MISSING = 'secrets_missing'; |
20
|
|
|
const SECRETS_EXPIRED = 'secrets_expired'; |
21
|
|
|
const SECRETS_OPTION_NAME = 'jetpack_secrets'; |
22
|
|
|
const MAGIC_NORMAL_TOKEN_KEY = ';normal;'; |
23
|
|
|
const JETPACK_MASTER_USER = true; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The procedure that should be run to generate secrets. |
27
|
|
|
* |
28
|
|
|
* @var Callable |
29
|
|
|
*/ |
30
|
|
|
protected $secret_callable; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* A copy of the raw POST data for signature verification purposes. |
34
|
|
|
* |
35
|
|
|
* @var String |
36
|
|
|
*/ |
37
|
|
|
protected $raw_post_data; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Verification data needs to be stored to properly verify everything. |
41
|
|
|
* |
42
|
|
|
* @var Object |
43
|
|
|
*/ |
44
|
|
|
private $xmlrpc_verification = null; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Initializes required listeners. This is done separately from the constructors |
48
|
|
|
* because some objects sometimes need to instantiate separate objects of this class. |
49
|
|
|
* |
50
|
|
|
* @todo Implement a proper nonce verification. |
51
|
|
|
*/ |
52
|
|
|
public function init() { |
53
|
|
|
$this->setup_xmlrpc_handlers( |
54
|
|
|
$_GET, // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
55
|
|
|
$this->is_active(), |
56
|
|
|
$this->verify_xml_rpc_signature() |
|
|
|
|
57
|
|
|
); |
58
|
|
|
|
59
|
|
|
if ( $this->is_active() ) { |
60
|
|
|
add_filter( 'xmlrpc_methods', array( $this, 'public_xmlrpc_methods' ) ); |
61
|
|
|
} else { |
62
|
|
|
add_action( 'rest_api_init', array( $this, 'initialize_rest_api_registration_connector' ) ); |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Sets up the XMLRPC request handlers. |
68
|
|
|
* |
69
|
|
|
* @param Array $request_params incoming request parameters. |
70
|
|
|
* @param Boolean $is_active whether the connection is currently active. |
71
|
|
|
* @param Boolean $is_signed whether the signature check has been successful. |
72
|
|
|
* @param \Jetpack_XMLRPC_Server $xmlrpc_server (optional) an instance of the server to use instead of instantiating a new one. |
|
|
|
|
73
|
|
|
*/ |
74
|
|
|
public function setup_xmlrpc_handlers( |
75
|
|
|
$request_params, |
76
|
|
|
$is_active, |
77
|
|
|
$is_signed, |
78
|
|
|
\Jetpack_XMLRPC_Server $xmlrpc_server = null |
79
|
|
|
) { |
80
|
|
|
if ( |
81
|
|
|
! isset( $request_params['for'] ) |
82
|
|
|
|| 'jetpack' !== $request_params['for'] |
83
|
|
|
) { |
84
|
|
|
return false; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// Alternate XML-RPC, via ?for=jetpack&jetpack=comms. |
88
|
|
|
if ( |
89
|
|
|
isset( $request_params['jetpack'] ) |
90
|
|
|
&& 'comms' === $request_params['jetpack'] |
91
|
|
|
) { |
92
|
|
|
if ( ! Constants::is_defined( 'XMLRPC_REQUEST' ) ) { |
93
|
|
|
// Use the real constant here for WordPress' sake. |
94
|
|
|
define( 'XMLRPC_REQUEST', true ); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
add_action( 'template_redirect', array( $this, 'alternate_xmlrpc' ) ); |
98
|
|
|
|
99
|
|
|
add_filter( 'xmlrpc_methods', array( $this, 'remove_non_jetpack_xmlrpc_methods' ), 1000 ); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
if ( ! Constants::get_constant( 'XMLRPC_REQUEST' ) ) { |
103
|
|
|
return false; |
104
|
|
|
} |
105
|
|
|
// Display errors can cause the XML to be not well formed. |
106
|
|
|
@ini_set( 'display_errors', false ); // phpcs:ignore |
|
|
|
|
107
|
|
|
|
108
|
|
|
if ( $xmlrpc_server ) { |
109
|
|
|
$this->xmlrpc_server = $xmlrpc_server; |
|
|
|
|
110
|
|
|
} else { |
111
|
|
|
$this->xmlrpc_server = new \Jetpack_XMLRPC_Server(); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
$this->require_jetpack_authentication(); |
115
|
|
|
|
116
|
|
|
if ( $is_active ) { |
117
|
|
|
// Hack to preserve $HTTP_RAW_POST_DATA. |
118
|
|
|
add_filter( 'xmlrpc_methods', array( $this, 'xmlrpc_methods' ) ); |
119
|
|
|
|
120
|
|
|
if ( $is_signed ) { |
121
|
|
|
// The actual API methods. |
122
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'xmlrpc_methods' ) ); |
123
|
|
|
} else { |
124
|
|
|
// The jetpack.authorize method should be available for unauthenticated users on a site with an |
125
|
|
|
// active Jetpack connection, so that additional users can link their account. |
126
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'authorize_xmlrpc_methods' ) ); |
127
|
|
|
} |
128
|
|
|
} else { |
129
|
|
|
// The bootstrap API methods. |
130
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'bootstrap_xmlrpc_methods' ) ); |
131
|
|
|
|
132
|
|
|
if ( $is_signed ) { |
133
|
|
|
// The jetpack Provision method is available for blog-token-signed requests. |
134
|
|
|
add_filter( 'xmlrpc_methods', array( $this->xmlrpc_server, 'provision_xmlrpc_methods' ) ); |
135
|
|
|
} else { |
136
|
|
|
new XMLRPC_Connector( $this ); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
add_filter( 'xmlrpc_blog_options', array( $this, 'xmlrpc_options' ) ); |
141
|
|
|
|
142
|
|
|
add_action( 'jetpack_clean_nonces', array( $this, 'clean_nonces' ) ); |
143
|
|
|
if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { |
144
|
|
|
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
// Now that no one can authenticate, and we're whitelisting all XML-RPC methods, force enable_xmlrpc on. |
148
|
|
|
add_filter( 'pre_option_enable_xmlrpc', '__return_true' ); |
149
|
|
|
|
150
|
|
|
return true; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Initializes the REST API connector on the init hook. |
155
|
|
|
*/ |
156
|
|
|
public function initialize_rest_api_registration_connector() { |
157
|
|
|
new REST_Connector( $this ); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Since a lot of hosts use a hammer approach to "protecting" WordPress sites, |
162
|
|
|
* and just blanket block all requests to /xmlrpc.php, or apply other overly-sensitive |
163
|
|
|
* security/firewall policies, we provide our own alternate XML RPC API endpoint |
164
|
|
|
* which is accessible via a different URI. Most of the below is copied directly |
165
|
|
|
* from /xmlrpc.php so that we're replicating it as closely as possible. |
166
|
|
|
* |
167
|
|
|
* @todo Tighten $wp_xmlrpc_server_class a bit to make sure it doesn't do bad things. |
168
|
|
|
*/ |
169
|
|
|
public function alternate_xmlrpc() { |
170
|
|
|
// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved |
171
|
|
|
// phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited |
172
|
|
|
global $HTTP_RAW_POST_DATA; |
173
|
|
|
|
174
|
|
|
// Some browser-embedded clients send cookies. We don't want them. |
175
|
|
|
$_COOKIE = array(); |
176
|
|
|
|
177
|
|
|
// A fix for mozBlog and other cases where '<?xml' isn't on the very first line. |
178
|
|
|
if ( isset( $HTTP_RAW_POST_DATA ) ) { |
179
|
|
|
$HTTP_RAW_POST_DATA = trim( $HTTP_RAW_POST_DATA ); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
// phpcs:enable |
183
|
|
|
|
184
|
|
|
include_once ABSPATH . 'wp-admin/includes/admin.php'; |
185
|
|
|
include_once ABSPATH . WPINC . '/class-IXR.php'; |
186
|
|
|
include_once ABSPATH . WPINC . '/class-wp-xmlrpc-server.php'; |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Filters the class used for handling XML-RPC requests. |
190
|
|
|
* |
191
|
|
|
* @since 3.1.0 |
192
|
|
|
* |
193
|
|
|
* @param string $class The name of the XML-RPC server class. |
194
|
|
|
*/ |
195
|
|
|
$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' ); |
196
|
|
|
$wp_xmlrpc_server = new $wp_xmlrpc_server_class(); |
197
|
|
|
|
198
|
|
|
// Fire off the request. |
199
|
|
|
nocache_headers(); |
200
|
|
|
$wp_xmlrpc_server->serve_request(); |
201
|
|
|
|
202
|
|
|
exit; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Removes all XML-RPC methods that are not `jetpack.*`. |
207
|
|
|
* Only used in our alternate XML-RPC endpoint, where we want to |
208
|
|
|
* ensure that Core and other plugins' methods are not exposed. |
209
|
|
|
* |
210
|
|
|
* @param array $methods a list of registered WordPress XMLRPC methods. |
211
|
|
|
* @return array filtered $methods |
212
|
|
|
*/ |
213
|
|
|
public function remove_non_jetpack_xmlrpc_methods( $methods ) { |
214
|
|
|
$jetpack_methods = array(); |
215
|
|
|
|
216
|
|
|
foreach ( $methods as $method => $callback ) { |
217
|
|
|
if ( 0 === strpos( $method, 'jetpack.' ) ) { |
218
|
|
|
$jetpack_methods[ $method ] = $callback; |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $jetpack_methods; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Removes all other authentication methods not to allow other |
227
|
|
|
* methods to validate unauthenticated requests. |
228
|
|
|
*/ |
229
|
|
|
public function require_jetpack_authentication() { |
230
|
|
|
// Don't let anyone authenticate. |
231
|
|
|
$_COOKIE = array(); |
232
|
|
|
remove_all_filters( 'authenticate' ); |
233
|
|
|
remove_all_actions( 'wp_login_failed' ); |
234
|
|
|
|
235
|
|
|
if ( $this->is_active() ) { |
236
|
|
|
// Allow Jetpack authentication. |
237
|
|
|
add_filter( 'authenticate', array( $this, 'authenticate_jetpack' ), 10, 3 ); |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Authenticates XML-RPC and other requests from the Jetpack Server |
243
|
|
|
* |
244
|
|
|
* @param WP_User|Mixed $user user object if authenticated. |
245
|
|
|
* @param String $username username. |
246
|
|
|
* @param String $password password string. |
247
|
|
|
* @return WP_User|Mixed authenticated user or error. |
248
|
|
|
*/ |
249
|
|
|
public function authenticate_jetpack( $user, $username, $password ) { |
250
|
|
|
if ( is_a( $user, '\\WP_User' ) ) { |
251
|
|
|
return $user; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$token_details = $this->verify_xml_rpc_signature(); |
255
|
|
|
|
256
|
|
|
if ( ! $token_details ) { |
257
|
|
|
return $user; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
if ( 'user' !== $token_details['type'] ) { |
261
|
|
|
return $user; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
if ( ! $token_details['user_id'] ) { |
265
|
|
|
return $user; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
nocache_headers(); |
269
|
|
|
|
270
|
|
|
return new \WP_User( $token_details['user_id'] ); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Verifies the signature of the current request. |
275
|
|
|
* |
276
|
|
|
* @return false|array |
277
|
|
|
*/ |
278
|
|
|
public function verify_xml_rpc_signature() { |
279
|
|
|
if ( is_null( $this->xmlrpc_verification ) ) { |
280
|
|
|
$this->xmlrpc_verification = $this->internal_verify_xml_rpc_signature(); |
281
|
|
|
|
282
|
|
|
if ( is_wp_error( $this->xmlrpc_verification ) ) { |
283
|
|
|
/** |
284
|
|
|
* Action for logging XMLRPC signature verification errors. This data is sensitive. |
285
|
|
|
* |
286
|
|
|
* Error codes: |
287
|
|
|
* - malformed_token |
288
|
|
|
* - malformed_user_id |
289
|
|
|
* - unknown_token |
290
|
|
|
* - could_not_sign |
291
|
|
|
* - invalid_nonce |
292
|
|
|
* - signature_mismatch |
293
|
|
|
* |
294
|
|
|
* @since 7.5.0 |
295
|
|
|
* |
296
|
|
|
* @param WP_Error $signature_verification_error The verification error |
297
|
|
|
*/ |
298
|
|
|
do_action( 'jetpack_verify_signature_error', $this->xmlrpc_verification ); |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
return is_wp_error( $this->xmlrpc_verification ) ? false : $this->xmlrpc_verification; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Verifies the signature of the current request. |
307
|
|
|
* |
308
|
|
|
* This function has side effects and should not be used. Instead, |
309
|
|
|
* use the memoized version `->verify_xml_rpc_signature()`. |
310
|
|
|
* |
311
|
|
|
* @internal |
312
|
|
|
*/ |
313
|
|
|
private function internal_verify_xml_rpc_signature() { |
314
|
|
|
// It's not for us. |
315
|
|
|
if ( ! isset( $_GET['token'] ) || empty( $_GET['signature'] ) ) { |
316
|
|
|
return false; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$signature_details = array( |
320
|
|
|
'token' => isset( $_GET['token'] ) ? wp_unslash( $_GET['token'] ) : '', |
321
|
|
|
'timestamp' => isset( $_GET['timestamp'] ) ? wp_unslash( $_GET['timestamp'] ) : '', |
322
|
|
|
'nonce' => isset( $_GET['nonce'] ) ? wp_unslash( $_GET['nonce'] ) : '', |
323
|
|
|
'body_hash' => isset( $_GET['body-hash'] ) ? wp_unslash( $_GET['body-hash'] ) : '', |
324
|
|
|
'method' => wp_unslash( $_SERVER['REQUEST_METHOD'] ), |
325
|
|
|
'url' => wp_unslash( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ), // Temp - will get real signature URL later. |
326
|
|
|
'signature' => isset( $_GET['signature'] ) ? wp_unslash( $_GET['signature'] ) : '', |
327
|
|
|
); |
328
|
|
|
|
329
|
|
|
@list( $token_key, $version, $user_id ) = explode( ':', wp_unslash( $_GET['token'] ) ); |
|
|
|
|
330
|
|
|
if ( |
331
|
|
|
empty( $token_key ) |
332
|
|
|
|| |
333
|
|
|
empty( $version ) || strval( JETPACK__API_VERSION ) !== $version |
334
|
|
|
) { |
335
|
|
|
return new \WP_Error( 'malformed_token', 'Malformed token in request', compact( 'signature_details' ) ); |
|
|
|
|
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
if ( '0' === $user_id ) { |
339
|
|
|
$token_type = 'blog'; |
340
|
|
|
$user_id = 0; |
341
|
|
|
} else { |
342
|
|
|
$token_type = 'user'; |
343
|
|
|
if ( empty( $user_id ) || ! ctype_digit( $user_id ) ) { |
344
|
|
|
return new \WP_Error( |
345
|
|
|
'malformed_user_id', |
|
|
|
|
346
|
|
|
'Malformed user_id in request', |
347
|
|
|
compact( 'signature_details' ) |
348
|
|
|
); |
349
|
|
|
} |
350
|
|
|
$user_id = (int) $user_id; |
351
|
|
|
|
352
|
|
|
$user = new \WP_User( $user_id ); |
353
|
|
|
if ( ! $user || ! $user->exists() ) { |
354
|
|
|
return new \WP_Error( |
355
|
|
|
'unknown_user', |
|
|
|
|
356
|
|
|
sprintf( 'User %d does not exist', $user_id ), |
357
|
|
|
compact( 'signature_details' ) |
358
|
|
|
); |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
$token = $this->get_access_token( $user_id, $token_key, false ); |
363
|
|
|
if ( is_wp_error( $token ) ) { |
364
|
|
|
$token->add_data( compact( 'signature_details' ) ); |
365
|
|
|
return $token; |
366
|
|
|
} elseif ( ! $token ) { |
367
|
|
|
return new \WP_Error( |
368
|
|
|
'unknown_token', |
|
|
|
|
369
|
|
|
sprintf( 'Token %s:%s:%d does not exist', $token_key, $version, $user_id ), |
370
|
|
|
compact( 'signature_details' ) |
371
|
|
|
); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
$jetpack_signature = new \Jetpack_Signature( $token->secret, (int) \Jetpack_Options::get_option( 'time_diff' ) ); |
375
|
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing |
376
|
|
|
if ( isset( $_POST['_jetpack_is_multipart'] ) ) { |
377
|
|
|
$post_data = $_POST; |
378
|
|
|
$file_hashes = array(); |
379
|
|
|
foreach ( $post_data as $post_data_key => $post_data_value ) { |
380
|
|
|
if ( 0 !== strpos( $post_data_key, '_jetpack_file_hmac_' ) ) { |
381
|
|
|
continue; |
382
|
|
|
} |
383
|
|
|
$post_data_key = substr( $post_data_key, strlen( '_jetpack_file_hmac_' ) ); |
384
|
|
|
$file_hashes[ $post_data_key ] = $post_data_value; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
foreach ( $file_hashes as $post_data_key => $post_data_value ) { |
388
|
|
|
unset( $post_data[ "_jetpack_file_hmac_{$post_data_key}" ] ); |
389
|
|
|
$post_data[ $post_data_key ] = $post_data_value; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
ksort( $post_data ); |
393
|
|
|
|
394
|
|
|
$body = http_build_query( stripslashes_deep( $post_data ) ); |
395
|
|
|
} elseif ( is_null( $this->raw_post_data ) ) { |
396
|
|
|
$body = file_get_contents( 'php://input' ); |
397
|
|
|
} else { |
398
|
|
|
$body = null; |
399
|
|
|
} |
400
|
|
|
// phpcs:enable |
401
|
|
|
|
402
|
|
|
$signature = $jetpack_signature->sign_current_request( |
403
|
|
|
array( 'body' => is_null( $body ) ? $this->raw_post_data : $body ) |
404
|
|
|
); |
405
|
|
|
|
406
|
|
|
$signature_details['url'] = $jetpack_signature->current_request_url; |
407
|
|
|
|
408
|
|
|
if ( ! $signature ) { |
409
|
|
|
return new \WP_Error( |
410
|
|
|
'could_not_sign', |
|
|
|
|
411
|
|
|
'Unknown signature error', |
412
|
|
|
compact( 'signature_details' ) |
413
|
|
|
); |
414
|
|
|
} elseif ( is_wp_error( $signature ) ) { |
415
|
|
|
return $signature; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
$timestamp = (int) $_GET['timestamp']; |
419
|
|
|
$nonce = stripslashes( (string) $_GET['nonce'] ); |
420
|
|
|
|
421
|
|
|
// Use up the nonce regardless of whether the signature matches. |
422
|
|
|
if ( ! $this->add_nonce( $timestamp, $nonce ) ) { |
423
|
|
|
return new \WP_Error( |
424
|
|
|
'invalid_nonce', |
|
|
|
|
425
|
|
|
'Could not add nonce', |
426
|
|
|
compact( 'signature_details' ) |
427
|
|
|
); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
// Be careful about what you do with this debugging data. |
431
|
|
|
// If a malicious requester has access to the expected signature, |
432
|
|
|
// bad things might be possible. |
433
|
|
|
$signature_details['expected'] = $signature; |
434
|
|
|
|
435
|
|
|
if ( ! hash_equals( $signature, $_GET['signature'] ) ) { |
436
|
|
|
return new \WP_Error( |
437
|
|
|
'signature_mismatch', |
|
|
|
|
438
|
|
|
'Signature mismatch', |
439
|
|
|
compact( 'signature_details' ) |
440
|
|
|
); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Action for additional token checking. |
445
|
|
|
* |
446
|
|
|
* @since 7.7.0 |
447
|
|
|
* |
448
|
|
|
* @param Array $post_data request data. |
449
|
|
|
* @param Array $token_data token data. |
450
|
|
|
*/ |
451
|
|
|
return apply_filters( |
452
|
|
|
'jetpack_signature_check_token', |
453
|
|
|
array( |
454
|
|
|
'type' => $token_type, |
455
|
|
|
'token_key' => $token_key, |
456
|
|
|
'user_id' => $token->external_user_id, |
457
|
|
|
), |
458
|
|
|
$token, |
459
|
|
|
$this->raw_post_data |
460
|
|
|
); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* Returns true if the current site is connected to WordPress.com. |
465
|
|
|
* |
466
|
|
|
* @return Boolean is the site connected? |
467
|
|
|
*/ |
468
|
|
|
public function is_active() { |
469
|
|
|
return (bool) $this->get_access_token( self::JETPACK_MASTER_USER ); |
|
|
|
|
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
/** |
473
|
|
|
* Returns true if the site has both a token and a blog id, which indicates a site has been registered. |
474
|
|
|
* |
475
|
|
|
* @access public |
476
|
|
|
* |
477
|
|
|
* @return bool |
478
|
|
|
*/ |
479
|
|
|
public function is_registered() { |
480
|
|
|
$blog_id = \Jetpack_Options::get_option( 'id' ); |
481
|
|
|
$has_token = $this->is_active(); |
482
|
|
|
return $blog_id && $has_token; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* Returns true if the user with the specified identifier is connected to |
487
|
|
|
* WordPress.com. |
488
|
|
|
* |
489
|
|
|
* @param Integer|Boolean $user_id the user identifier. |
490
|
|
|
* @return Boolean is the user connected? |
491
|
|
|
*/ |
492
|
|
|
public function is_user_connected( $user_id = false ) { |
493
|
|
|
$user_id = false === $user_id ? get_current_user_id() : absint( $user_id ); |
494
|
|
|
if ( ! $user_id ) { |
495
|
|
|
return false; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
return (bool) $this->get_access_token( $user_id ); |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Get the wpcom user data of the current|specified connected user. |
503
|
|
|
* |
504
|
|
|
* @todo Refactor to properly load the XMLRPC client independently. |
505
|
|
|
* |
506
|
|
|
* @param Integer $user_id the user identifier. |
|
|
|
|
507
|
|
|
* @return Object the user object. |
508
|
|
|
*/ |
509
|
|
View Code Duplication |
public function get_connected_user_data( $user_id = null ) { |
510
|
|
|
if ( ! $user_id ) { |
|
|
|
|
511
|
|
|
$user_id = get_current_user_id(); |
512
|
|
|
} |
513
|
|
|
|
514
|
|
|
$transient_key = "jetpack_connected_user_data_$user_id"; |
515
|
|
|
$cached_user_data = get_transient( $transient_key ); |
516
|
|
|
|
517
|
|
|
if ( $cached_user_data ) { |
518
|
|
|
return $cached_user_data; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
\Jetpack::load_xml_rpc_client(); |
522
|
|
|
$xml = new \Jetpack_IXR_Client( |
523
|
|
|
array( |
524
|
|
|
'user_id' => $user_id, |
525
|
|
|
) |
526
|
|
|
); |
527
|
|
|
$xml->query( 'wpcom.getUser' ); |
528
|
|
|
if ( ! $xml->isError() ) { |
529
|
|
|
$user_data = $xml->getResponse(); |
530
|
|
|
set_transient( $transient_key, $xml->getResponse(), DAY_IN_SECONDS ); |
531
|
|
|
return $user_data; |
532
|
|
|
} |
533
|
|
|
|
534
|
|
|
return false; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* Returns true if the provided user is the Jetpack connection owner. |
539
|
|
|
* If user ID is not specified, the current user will be used. |
540
|
|
|
* |
541
|
|
|
* @param Integer|Boolean $user_id the user identifier. False for current user. |
542
|
|
|
* @return Boolean True the user the connection owner, false otherwise. |
543
|
|
|
*/ |
544
|
|
|
public function is_connection_owner( $user_id = false ) { |
545
|
|
|
if ( ! $user_id ) { |
546
|
|
|
$user_id = get_current_user_id(); |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
$user_token = $this->get_access_token( JETPACK_MASTER_USER ); |
|
|
|
|
550
|
|
|
|
551
|
|
|
return $user_token && is_object( $user_token ) && isset( $user_token->external_user_id ) && $user_id === $user_token->external_user_id; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Unlinks the current user from the linked WordPress.com user. |
556
|
|
|
* |
557
|
|
|
* @access public |
558
|
|
|
* @static |
559
|
|
|
* |
560
|
|
|
* @todo Refactor to properly load the XMLRPC client independently. |
561
|
|
|
* |
562
|
|
|
* @param Integer $user_id the user identifier. |
|
|
|
|
563
|
|
|
* @return Boolean Whether the disconnection of the user was successful. |
564
|
|
|
*/ |
565
|
|
|
public static function disconnect_user( $user_id = null ) { |
566
|
|
|
$tokens = \Jetpack_Options::get_option( 'user_tokens' ); |
567
|
|
|
if ( ! $tokens ) { |
568
|
|
|
return false; |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
$user_id = empty( $user_id ) ? get_current_user_id() : intval( $user_id ); |
572
|
|
|
|
573
|
|
|
if ( \Jetpack_Options::get_option( 'master_user' ) === $user_id ) { |
574
|
|
|
return false; |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
if ( ! isset( $tokens[ $user_id ] ) ) { |
578
|
|
|
return false; |
579
|
|
|
} |
580
|
|
|
|
581
|
|
|
\Jetpack::load_xml_rpc_client(); |
582
|
|
|
$xml = new \Jetpack_IXR_Client( compact( 'user_id' ) ); |
583
|
|
|
$xml->query( 'jetpack.unlink_user', $user_id ); |
584
|
|
|
|
585
|
|
|
unset( $tokens[ $user_id ] ); |
586
|
|
|
|
587
|
|
|
\Jetpack_Options::update_option( 'user_tokens', $tokens ); |
588
|
|
|
|
589
|
|
|
/** |
590
|
|
|
* Fires after the current user has been unlinked from WordPress.com. |
591
|
|
|
* |
592
|
|
|
* @since 4.1.0 |
593
|
|
|
* |
594
|
|
|
* @param int $user_id The current user's ID. |
595
|
|
|
*/ |
596
|
|
|
do_action( 'jetpack_unlinked_user', $user_id ); |
597
|
|
|
|
598
|
|
|
return true; |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* Returns the requested Jetpack API URL. |
603
|
|
|
* |
604
|
|
|
* @param String $relative_url the relative API path. |
605
|
|
|
* @return String API URL. |
606
|
|
|
*/ |
607
|
|
|
public function api_url( $relative_url ) { |
608
|
|
|
$api_base = Constants::get_constant( 'JETPACK__API_BASE' ); |
609
|
|
|
$version = Constants::get_constant( 'JETPACK__API_VERSION' ); |
610
|
|
|
|
611
|
|
|
$api_base = $api_base ? $api_base : 'https://jetpack.wordpress.com/jetpack.'; |
612
|
|
|
$version = $version ? '/' . $version . '/' : '/1/'; |
613
|
|
|
|
614
|
|
|
return rtrim( $api_base . $relative_url, '/\\' ) . $version; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
/** |
618
|
|
|
* Attempts Jetpack registration which sets up the site for connection. Should |
619
|
|
|
* remain public because the call to action comes from the current site, not from |
620
|
|
|
* WordPress.com. |
621
|
|
|
* |
622
|
|
|
* @param String $api_endpoint (optional) an API endpoint to use, defaults to 'register'. |
623
|
|
|
* @return Integer zero on success, or a bitmask on failure. |
624
|
|
|
*/ |
625
|
|
|
public function register( $api_endpoint = 'register' ) { |
626
|
|
|
add_action( 'pre_update_jetpack_option_register', array( '\\Jetpack_Options', 'delete_option' ) ); |
627
|
|
|
$secrets = $this->generate_secrets( 'register', get_current_user_id(), 600 ); |
628
|
|
|
|
629
|
|
|
if ( |
630
|
|
|
empty( $secrets['secret_1'] ) || |
631
|
|
|
empty( $secrets['secret_2'] ) || |
632
|
|
|
empty( $secrets['exp'] ) |
633
|
|
|
) { |
634
|
|
|
return new \WP_Error( 'missing_secrets' ); |
|
|
|
|
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
// Better to try (and fail) to set a higher timeout than this system |
638
|
|
|
// supports than to have register fail for more users than it should. |
639
|
|
|
$timeout = $this->set_min_time_limit( 60 ) / 2; |
640
|
|
|
|
641
|
|
|
$gmt_offset = get_option( 'gmt_offset' ); |
642
|
|
|
if ( ! $gmt_offset ) { |
643
|
|
|
$gmt_offset = 0; |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
$stats_options = get_option( 'stats_options' ); |
647
|
|
|
$stats_id = isset( $stats_options['blog_id'] ) |
648
|
|
|
? $stats_options['blog_id'] |
649
|
|
|
: null; |
650
|
|
|
|
651
|
|
|
/** |
652
|
|
|
* Filters the request body for additional property addition. |
653
|
|
|
* |
654
|
|
|
* @since 7.7.0 |
655
|
|
|
* |
656
|
|
|
* @param Array $post_data request data. |
657
|
|
|
* @param Array $token_data token data. |
658
|
|
|
*/ |
659
|
|
|
$body = apply_filters( |
660
|
|
|
'jetpack_register_request_body', |
661
|
|
|
array( |
662
|
|
|
'siteurl' => site_url(), |
663
|
|
|
'home' => home_url(), |
664
|
|
|
'gmt_offset' => $gmt_offset, |
665
|
|
|
'timezone_string' => (string) get_option( 'timezone_string' ), |
666
|
|
|
'site_name' => (string) get_option( 'blogname' ), |
667
|
|
|
'secret_1' => $secrets['secret_1'], |
668
|
|
|
'secret_2' => $secrets['secret_2'], |
669
|
|
|
'site_lang' => get_locale(), |
670
|
|
|
'timeout' => $timeout, |
671
|
|
|
'stats_id' => $stats_id, |
672
|
|
|
'state' => get_current_user_id(), |
673
|
|
|
'site_created' => $this->get_assumed_site_creation_date(), |
674
|
|
|
'jetpack_version' => Constants::get_constant( 'JETPACK__VERSION' ), |
675
|
|
|
) |
676
|
|
|
); |
677
|
|
|
|
678
|
|
|
$args = array( |
679
|
|
|
'method' => 'POST', |
680
|
|
|
'body' => $body, |
681
|
|
|
'headers' => array( |
682
|
|
|
'Accept' => 'application/json', |
683
|
|
|
), |
684
|
|
|
'timeout' => $timeout, |
685
|
|
|
); |
686
|
|
|
|
687
|
|
|
$args['body'] = $this->apply_activation_source_to_args( $args['body'] ); |
688
|
|
|
|
689
|
|
|
// TODO: fix URLs for bad hosts. |
690
|
|
|
$response = Client::_wp_remote_request( |
691
|
|
|
$this->api_url( $api_endpoint ), |
692
|
|
|
$args, |
693
|
|
|
true |
694
|
|
|
); |
695
|
|
|
|
696
|
|
|
// Make sure the response is valid and does not contain any Jetpack errors. |
697
|
|
|
$registration_details = $this->validate_remote_register_response( $response ); |
698
|
|
|
|
699
|
|
|
if ( is_wp_error( $registration_details ) ) { |
700
|
|
|
return $registration_details; |
701
|
|
|
} elseif ( ! $registration_details ) { |
702
|
|
|
return new \WP_Error( |
703
|
|
|
'unknown_error', |
|
|
|
|
704
|
|
|
'Unknown error registering your Jetpack site.', |
705
|
|
|
wp_remote_retrieve_response_code( $response ) |
706
|
|
|
); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
if ( empty( $registration_details->jetpack_secret ) || ! is_string( $registration_details->jetpack_secret ) ) { |
710
|
|
|
return new \WP_Error( |
711
|
|
|
'jetpack_secret', |
|
|
|
|
712
|
|
|
'Unable to validate registration of your Jetpack site.', |
713
|
|
|
wp_remote_retrieve_response_code( $response ) |
714
|
|
|
); |
715
|
|
|
} |
716
|
|
|
|
717
|
|
|
if ( isset( $registration_details->jetpack_public ) ) { |
718
|
|
|
$jetpack_public = (int) $registration_details->jetpack_public; |
719
|
|
|
} else { |
720
|
|
|
$jetpack_public = false; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
\Jetpack_Options::update_options( |
724
|
|
|
array( |
725
|
|
|
'id' => (int) $registration_details->jetpack_id, |
726
|
|
|
'blog_token' => (string) $registration_details->jetpack_secret, |
727
|
|
|
'public' => $jetpack_public, |
728
|
|
|
) |
729
|
|
|
); |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Fires when a site is registered on WordPress.com. |
733
|
|
|
* |
734
|
|
|
* @since 3.7.0 |
735
|
|
|
* |
736
|
|
|
* @param int $json->jetpack_id Jetpack Blog ID. |
737
|
|
|
* @param string $json->jetpack_secret Jetpack Blog Token. |
738
|
|
|
* @param int|bool $jetpack_public Is the site public. |
739
|
|
|
*/ |
740
|
|
|
do_action( |
741
|
|
|
'jetpack_site_registered', |
742
|
|
|
$registration_details->jetpack_id, |
743
|
|
|
$registration_details->jetpack_secret, |
744
|
|
|
$jetpack_public |
745
|
|
|
); |
746
|
|
|
|
747
|
|
|
if ( isset( $registration_details->token ) ) { |
748
|
|
|
/** |
749
|
|
|
* Fires when a user token is sent along with the registration data. |
750
|
|
|
* |
751
|
|
|
* @since 7.6.0 |
752
|
|
|
* |
753
|
|
|
* @param object $token the administrator token for the newly registered site. |
754
|
|
|
*/ |
755
|
|
|
do_action( 'jetpack_site_registered_user_token', $registration_details->token ); |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
return true; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Takes the response from the Jetpack register new site endpoint and |
763
|
|
|
* verifies it worked properly. |
764
|
|
|
* |
765
|
|
|
* @since 2.6 |
766
|
|
|
* |
767
|
|
|
* @param Mixed $response the response object, or the error object. |
768
|
|
|
* @return string|WP_Error A JSON object on success or Jetpack_Error on failures |
769
|
|
|
**/ |
770
|
|
|
protected function validate_remote_register_response( $response ) { |
771
|
|
|
if ( is_wp_error( $response ) ) { |
772
|
|
|
return new \WP_Error( |
773
|
|
|
'register_http_request_failed', |
|
|
|
|
774
|
|
|
$response->get_error_message() |
775
|
|
|
); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
$code = wp_remote_retrieve_response_code( $response ); |
779
|
|
|
$entity = wp_remote_retrieve_body( $response ); |
780
|
|
|
|
781
|
|
|
if ( $entity ) { |
782
|
|
|
$registration_response = json_decode( $entity ); |
783
|
|
|
} else { |
784
|
|
|
$registration_response = false; |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
$code_type = intval( $code / 100 ); |
788
|
|
|
if ( 5 === $code_type ) { |
789
|
|
|
return new \WP_Error( 'wpcom_5??', $code ); |
|
|
|
|
790
|
|
|
} elseif ( 408 === $code ) { |
791
|
|
|
return new \WP_Error( 'wpcom_408', $code ); |
|
|
|
|
792
|
|
|
} elseif ( ! empty( $registration_response->error ) ) { |
793
|
|
|
if ( |
794
|
|
|
'xml_rpc-32700' === $registration_response->error |
795
|
|
|
&& ! function_exists( 'xml_parser_create' ) |
796
|
|
|
) { |
797
|
|
|
$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' ); |
798
|
|
|
} else { |
799
|
|
|
$error_description = isset( $registration_response->error_description ) |
800
|
|
|
? (string) $registration_response->error_description |
801
|
|
|
: ''; |
802
|
|
|
} |
803
|
|
|
|
804
|
|
|
return new \WP_Error( |
805
|
|
|
(string) $registration_response->error, |
|
|
|
|
806
|
|
|
$error_description, |
807
|
|
|
$code |
808
|
|
|
); |
809
|
|
|
} elseif ( 200 !== $code ) { |
810
|
|
|
return new \WP_Error( 'wpcom_bad_response', $code ); |
|
|
|
|
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
// Jetpack ID error block. |
814
|
|
|
if ( empty( $registration_response->jetpack_id ) ) { |
815
|
|
|
return new \WP_Error( |
816
|
|
|
'jetpack_id', |
|
|
|
|
817
|
|
|
/* translators: %s is an error message string */ |
818
|
|
|
sprintf( __( 'Error Details: Jetpack ID is empty. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
819
|
|
|
$entity |
820
|
|
|
); |
821
|
|
|
} elseif ( ! is_scalar( $registration_response->jetpack_id ) ) { |
822
|
|
|
return new \WP_Error( |
823
|
|
|
'jetpack_id', |
|
|
|
|
824
|
|
|
/* translators: %s is an error message string */ |
825
|
|
|
sprintf( __( 'Error Details: Jetpack ID is not a scalar. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
826
|
|
|
$entity |
827
|
|
|
); |
828
|
|
|
} elseif ( preg_match( '/[^0-9]/', $registration_response->jetpack_id ) ) { |
829
|
|
|
return new \WP_Error( |
830
|
|
|
'jetpack_id', |
|
|
|
|
831
|
|
|
/* translators: %s is an error message string */ |
832
|
|
|
sprintf( __( 'Error Details: Jetpack ID begins with a numeral. Do not publicly post this error message! %s', 'jetpack' ), $entity ), |
833
|
|
|
$entity |
834
|
|
|
); |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
return $registration_response; |
838
|
|
|
} |
839
|
|
|
|
840
|
|
|
/** |
841
|
|
|
* Adds a used nonce to a list of known nonces. |
842
|
|
|
* |
843
|
|
|
* @param int $timestamp the current request timestamp. |
844
|
|
|
* @param string $nonce the nonce value. |
845
|
|
|
* @return bool whether the nonce is unique or not. |
846
|
|
|
*/ |
847
|
|
|
public function add_nonce( $timestamp, $nonce ) { |
848
|
|
|
global $wpdb; |
849
|
|
|
static $nonces_used_this_request = array(); |
850
|
|
|
|
851
|
|
|
if ( isset( $nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { |
852
|
|
|
return $nonces_used_this_request[ "$timestamp:$nonce" ]; |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp an $nonce. |
856
|
|
|
$timestamp = (int) $timestamp; |
857
|
|
|
$nonce = esc_sql( $nonce ); |
858
|
|
|
|
859
|
|
|
// Raw query so we can avoid races: add_option will also update. |
860
|
|
|
$show_errors = $wpdb->show_errors( false ); |
861
|
|
|
|
862
|
|
|
$old_nonce = $wpdb->get_row( |
863
|
|
|
$wpdb->prepare( "SELECT * FROM `$wpdb->options` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) |
864
|
|
|
); |
865
|
|
|
|
866
|
|
|
if ( is_null( $old_nonce ) ) { |
867
|
|
|
$return = $wpdb->query( |
868
|
|
|
$wpdb->prepare( |
869
|
|
|
"INSERT INTO `$wpdb->options` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", |
870
|
|
|
"jetpack_nonce_{$timestamp}_{$nonce}", |
871
|
|
|
time(), |
872
|
|
|
'no' |
873
|
|
|
) |
874
|
|
|
); |
875
|
|
|
} else { |
876
|
|
|
$return = false; |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
$wpdb->show_errors( $show_errors ); |
880
|
|
|
|
881
|
|
|
$nonces_used_this_request[ "$timestamp:$nonce" ] = $return; |
882
|
|
|
|
883
|
|
|
return $return; |
884
|
|
|
} |
885
|
|
|
|
886
|
|
|
/** |
887
|
|
|
* Cleans nonces that were saved when calling ::add_nonce. |
888
|
|
|
* |
889
|
|
|
* @todo Properly prepare the query before executing it. |
890
|
|
|
* |
891
|
|
|
* @param bool $all whether to clean even non-expired nonces. |
892
|
|
|
*/ |
893
|
|
|
public function clean_nonces( $all = false ) { |
894
|
|
|
global $wpdb; |
895
|
|
|
|
896
|
|
|
$sql = "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s"; |
897
|
|
|
$sql_args = array( $wpdb->esc_like( 'jetpack_nonce_' ) . '%' ); |
898
|
|
|
|
899
|
|
|
if ( true !== $all ) { |
900
|
|
|
$sql .= ' AND CAST( `option_value` AS UNSIGNED ) < %d'; |
901
|
|
|
$sql_args[] = time() - 3600; |
902
|
|
|
} |
903
|
|
|
|
904
|
|
|
$sql .= ' ORDER BY `option_id` LIMIT 100'; |
905
|
|
|
|
906
|
|
|
$sql = $wpdb->prepare( $sql, $sql_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
907
|
|
|
|
908
|
|
|
for ( $i = 0; $i < 1000; $i++ ) { |
909
|
|
|
if ( ! $wpdb->query( $sql ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
910
|
|
|
break; |
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
/** |
916
|
|
|
* Builds the timeout limit for queries talking with the wpcom servers. |
917
|
|
|
* |
918
|
|
|
* Based on local php max_execution_time in php.ini |
919
|
|
|
* |
920
|
|
|
* @since 5.4 |
921
|
|
|
* @return int |
922
|
|
|
**/ |
923
|
|
|
public function get_max_execution_time() { |
924
|
|
|
$timeout = (int) ini_get( 'max_execution_time' ); |
925
|
|
|
|
926
|
|
|
// Ensure exec time set in php.ini. |
927
|
|
|
if ( ! $timeout ) { |
928
|
|
|
$timeout = 30; |
929
|
|
|
} |
930
|
|
|
return $timeout; |
931
|
|
|
} |
932
|
|
|
|
933
|
|
|
/** |
934
|
|
|
* Sets a minimum request timeout, and returns the current timeout |
935
|
|
|
* |
936
|
|
|
* @since 5.4 |
937
|
|
|
* @param Integer $min_timeout the minimum timeout value. |
938
|
|
|
**/ |
939
|
|
View Code Duplication |
public function set_min_time_limit( $min_timeout ) { |
940
|
|
|
$timeout = $this->get_max_execution_time(); |
941
|
|
|
if ( $timeout < $min_timeout ) { |
942
|
|
|
$timeout = $min_timeout; |
943
|
|
|
set_time_limit( $timeout ); |
944
|
|
|
} |
945
|
|
|
return $timeout; |
946
|
|
|
} |
947
|
|
|
|
948
|
|
|
/** |
949
|
|
|
* Get our assumed site creation date. |
950
|
|
|
* Calculated based on the earlier date of either: |
951
|
|
|
* - Earliest admin user registration date. |
952
|
|
|
* - Earliest date of post of any post type. |
953
|
|
|
* |
954
|
|
|
* @since 7.2.0 |
955
|
|
|
* |
956
|
|
|
* @return string Assumed site creation date and time. |
957
|
|
|
*/ |
958
|
|
View Code Duplication |
public function get_assumed_site_creation_date() { |
959
|
|
|
$earliest_registered_users = get_users( |
960
|
|
|
array( |
961
|
|
|
'role' => 'administrator', |
962
|
|
|
'orderby' => 'user_registered', |
963
|
|
|
'order' => 'ASC', |
964
|
|
|
'fields' => array( 'user_registered' ), |
965
|
|
|
'number' => 1, |
966
|
|
|
) |
967
|
|
|
); |
968
|
|
|
$earliest_registration_date = $earliest_registered_users[0]->user_registered; |
969
|
|
|
|
970
|
|
|
$earliest_posts = get_posts( |
971
|
|
|
array( |
972
|
|
|
'posts_per_page' => 1, |
973
|
|
|
'post_type' => 'any', |
974
|
|
|
'post_status' => 'any', |
975
|
|
|
'orderby' => 'date', |
976
|
|
|
'order' => 'ASC', |
977
|
|
|
) |
978
|
|
|
); |
979
|
|
|
|
980
|
|
|
// If there are no posts at all, we'll count only on user registration date. |
981
|
|
|
if ( $earliest_posts ) { |
982
|
|
|
$earliest_post_date = $earliest_posts[0]->post_date; |
983
|
|
|
} else { |
984
|
|
|
$earliest_post_date = PHP_INT_MAX; |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
return min( $earliest_registration_date, $earliest_post_date ); |
988
|
|
|
} |
989
|
|
|
|
990
|
|
|
/** |
991
|
|
|
* Adds the activation source string as a parameter to passed arguments. |
992
|
|
|
* |
993
|
|
|
* @param Array $args arguments that need to have the source added. |
994
|
|
|
* @return Array $amended arguments. |
995
|
|
|
*/ |
996
|
|
View Code Duplication |
public static function apply_activation_source_to_args( $args ) { |
997
|
|
|
list( $activation_source_name, $activation_source_keyword ) = get_option( 'jetpack_activation_source' ); |
998
|
|
|
|
999
|
|
|
if ( $activation_source_name ) { |
1000
|
|
|
$args['_as'] = urlencode( $activation_source_name ); |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
if ( $activation_source_keyword ) { |
1004
|
|
|
$args['_ak'] = urlencode( $activation_source_keyword ); |
1005
|
|
|
} |
1006
|
|
|
|
1007
|
|
|
return $args; |
1008
|
|
|
} |
1009
|
|
|
|
1010
|
|
|
/** |
1011
|
|
|
* Returns the callable that would be used to generate secrets. |
1012
|
|
|
* |
1013
|
|
|
* @return Callable a function that returns a secure string to be used as a secret. |
1014
|
|
|
*/ |
1015
|
|
|
protected function get_secret_callable() { |
1016
|
|
|
if ( ! isset( $this->secret_callable ) ) { |
1017
|
|
|
/** |
1018
|
|
|
* Allows modification of the callable that is used to generate connection secrets. |
1019
|
|
|
* |
1020
|
|
|
* @param Callable a function or method that returns a secret string. |
1021
|
|
|
*/ |
1022
|
|
|
$this->secret_callable = apply_filters( 'jetpack_connection_secret_generator', 'wp_generate_password' ); |
1023
|
|
|
} |
1024
|
|
|
|
1025
|
|
|
return $this->secret_callable; |
1026
|
|
|
} |
1027
|
|
|
|
1028
|
|
|
/** |
1029
|
|
|
* Generates two secret tokens and the end of life timestamp for them. |
1030
|
|
|
* |
1031
|
|
|
* @param String $action The action name. |
1032
|
|
|
* @param Integer $user_id The user identifier. |
1033
|
|
|
* @param Integer $exp Expiration time in seconds. |
1034
|
|
|
*/ |
1035
|
|
|
public function generate_secrets( $action, $user_id, $exp ) { |
1036
|
|
|
$callable = $this->get_secret_callable(); |
1037
|
|
|
|
1038
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1039
|
|
|
self::SECRETS_OPTION_NAME, |
1040
|
|
|
array() |
1041
|
|
|
); |
1042
|
|
|
|
1043
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1044
|
|
|
|
1045
|
|
|
if ( |
1046
|
|
|
isset( $secrets[ $secret_name ] ) && |
1047
|
|
|
$secrets[ $secret_name ]['exp'] > time() |
1048
|
|
|
) { |
1049
|
|
|
return $secrets[ $secret_name ]; |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
|
|
$secret_value = array( |
1053
|
|
|
'secret_1' => call_user_func( $callable ), |
1054
|
|
|
'secret_2' => call_user_func( $callable ), |
1055
|
|
|
'exp' => time() + $exp, |
1056
|
|
|
); |
1057
|
|
|
|
1058
|
|
|
$secrets[ $secret_name ] = $secret_value; |
1059
|
|
|
|
1060
|
|
|
\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); |
1061
|
|
|
return $secrets[ $secret_name ]; |
1062
|
|
|
} |
1063
|
|
|
|
1064
|
|
|
/** |
1065
|
|
|
* Returns two secret tokens and the end of life timestamp for them. |
1066
|
|
|
* |
1067
|
|
|
* @param String $action The action name. |
1068
|
|
|
* @param Integer $user_id The user identifier. |
1069
|
|
|
* @return string|array an array of secrets or an error string. |
1070
|
|
|
*/ |
1071
|
|
|
public function get_secrets( $action, $user_id ) { |
1072
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1073
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1074
|
|
|
self::SECRETS_OPTION_NAME, |
1075
|
|
|
array() |
1076
|
|
|
); |
1077
|
|
|
|
1078
|
|
|
if ( ! isset( $secrets[ $secret_name ] ) ) { |
1079
|
|
|
return self::SECRETS_MISSING; |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
if ( $secrets[ $secret_name ]['exp'] < time() ) { |
1083
|
|
|
$this->delete_secrets( $action, $user_id ); |
1084
|
|
|
return self::SECRETS_EXPIRED; |
1085
|
|
|
} |
1086
|
|
|
|
1087
|
|
|
return $secrets[ $secret_name ]; |
1088
|
|
|
} |
1089
|
|
|
|
1090
|
|
|
/** |
1091
|
|
|
* Deletes secret tokens in case they, for example, have expired. |
1092
|
|
|
* |
1093
|
|
|
* @param String $action The action name. |
1094
|
|
|
* @param Integer $user_id The user identifier. |
1095
|
|
|
*/ |
1096
|
|
|
public function delete_secrets( $action, $user_id ) { |
1097
|
|
|
$secret_name = 'jetpack_' . $action . '_' . $user_id; |
1098
|
|
|
$secrets = \Jetpack_Options::get_raw_option( |
1099
|
|
|
self::SECRETS_OPTION_NAME, |
1100
|
|
|
array() |
1101
|
|
|
); |
1102
|
|
|
if ( isset( $secrets[ $secret_name ] ) ) { |
1103
|
|
|
unset( $secrets[ $secret_name ] ); |
1104
|
|
|
\Jetpack_Options::update_raw_option( self::SECRETS_OPTION_NAME, $secrets ); |
1105
|
|
|
} |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
/** |
1109
|
|
|
* Responds to a WordPress.com call to register the current site. |
1110
|
|
|
* Should be changed to protected. |
1111
|
|
|
* |
1112
|
|
|
* @param array $registration_data Array of [ secret_1, user_id ]. |
1113
|
|
|
*/ |
1114
|
|
|
public function handle_registration( array $registration_data ) { |
1115
|
|
|
list( $registration_secret_1, $registration_user_id ) = $registration_data; |
1116
|
|
|
if ( empty( $registration_user_id ) ) { |
1117
|
|
|
return new \WP_Error( 'registration_state_invalid', __( 'Invalid Registration State', 'jetpack' ), 400 ); |
|
|
|
|
1118
|
|
|
} |
1119
|
|
|
|
1120
|
|
|
return $this->verify_secrets( 'register', $registration_secret_1, (int) $registration_user_id ); |
1121
|
|
|
} |
1122
|
|
|
|
1123
|
|
|
/** |
1124
|
|
|
* Verify a Previously Generated Secret. |
1125
|
|
|
* |
1126
|
|
|
* @param string $action The type of secret to verify. |
1127
|
|
|
* @param string $secret_1 The secret string to compare to what is stored. |
1128
|
|
|
* @param int $user_id The user ID of the owner of the secret. |
1129
|
|
|
*/ |
1130
|
|
|
protected function verify_secrets( $action, $secret_1, $user_id ) { |
1131
|
|
|
$allowed_actions = array( 'register', 'authorize', 'publicize' ); |
1132
|
|
|
if ( ! in_array( $action, $allowed_actions, true ) ) { |
1133
|
|
|
return new \WP_Error( 'unknown_verification_action', 'Unknown Verification Action', 400 ); |
|
|
|
|
1134
|
|
|
} |
1135
|
|
|
|
1136
|
|
|
$user = get_user_by( 'id', $user_id ); |
1137
|
|
|
|
1138
|
|
|
/** |
1139
|
|
|
* We've begun verifying the previously generated secret. |
1140
|
|
|
* |
1141
|
|
|
* @since 7.5.0 |
1142
|
|
|
* |
1143
|
|
|
* @param string $action The type of secret to verify. |
1144
|
|
|
* @param \WP_User $user The user object. |
1145
|
|
|
*/ |
1146
|
|
|
do_action( 'jetpack_verify_secrets_begin', $action, $user ); |
1147
|
|
|
|
1148
|
|
|
$return_error = function( \WP_Error $error ) use ( $action, $user ) { |
1149
|
|
|
/** |
1150
|
|
|
* Verifying of the previously generated secret has failed. |
1151
|
|
|
* |
1152
|
|
|
* @since 7.5.0 |
1153
|
|
|
* |
1154
|
|
|
* @param string $action The type of secret to verify. |
1155
|
|
|
* @param \WP_User $user The user object. |
1156
|
|
|
* @param \WP_Error $error The error object. |
1157
|
|
|
*/ |
1158
|
|
|
do_action( 'jetpack_verify_secrets_fail', $action, $user, $error ); |
1159
|
|
|
|
1160
|
|
|
return $error; |
1161
|
|
|
}; |
1162
|
|
|
|
1163
|
|
|
$stored_secrets = $this->get_secrets( $action, $user_id ); |
1164
|
|
|
$this->delete_secrets( $action, $user_id ); |
1165
|
|
|
|
1166
|
|
|
if ( empty( $secret_1 ) ) { |
1167
|
|
|
return $return_error( |
1168
|
|
|
new \WP_Error( |
1169
|
|
|
'verify_secret_1_missing', |
|
|
|
|
1170
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1171
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'secret_1' ), |
1172
|
|
|
400 |
1173
|
|
|
) |
1174
|
|
|
); |
1175
|
|
|
} elseif ( ! is_string( $secret_1 ) ) { |
1176
|
|
|
return $return_error( |
1177
|
|
|
new \WP_Error( |
1178
|
|
|
'verify_secret_1_malformed', |
|
|
|
|
1179
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1180
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'secret_1' ), |
1181
|
|
|
400 |
1182
|
|
|
) |
1183
|
|
|
); |
1184
|
|
|
} elseif ( empty( $user_id ) ) { |
1185
|
|
|
// $user_id is passed around during registration as "state". |
1186
|
|
|
return $return_error( |
1187
|
|
|
new \WP_Error( |
1188
|
|
|
'state_missing', |
|
|
|
|
1189
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1190
|
|
|
sprintf( __( 'The required "%s" parameter is missing.', 'jetpack' ), 'state' ), |
1191
|
|
|
400 |
1192
|
|
|
) |
1193
|
|
|
); |
1194
|
|
|
} elseif ( ! ctype_digit( (string) $user_id ) ) { |
1195
|
|
|
return $return_error( |
1196
|
|
|
new \WP_Error( |
1197
|
|
|
'verify_secret_1_malformed', |
|
|
|
|
1198
|
|
|
/* translators: "%s" is the name of a paramter. It can be either "secret_1" or "state". */ |
1199
|
|
|
sprintf( __( 'The required "%s" parameter is malformed.', 'jetpack' ), 'state' ), |
1200
|
|
|
400 |
1201
|
|
|
) |
1202
|
|
|
); |
1203
|
|
|
} |
1204
|
|
|
|
1205
|
|
|
if ( ! $stored_secrets ) { |
1206
|
|
|
return $return_error( |
1207
|
|
|
new \WP_Error( |
1208
|
|
|
'verify_secrets_missing', |
|
|
|
|
1209
|
|
|
__( 'Verification secrets not found', 'jetpack' ), |
1210
|
|
|
400 |
1211
|
|
|
) |
1212
|
|
|
); |
1213
|
|
|
} elseif ( is_wp_error( $stored_secrets ) ) { |
1214
|
|
|
$stored_secrets->add_data( 400 ); |
|
|
|
|
1215
|
|
|
return $return_error( $stored_secrets ); |
1216
|
|
|
} elseif ( empty( $stored_secrets['secret_1'] ) || empty( $stored_secrets['secret_2'] ) || empty( $stored_secrets['exp'] ) ) { |
1217
|
|
|
return $return_error( |
1218
|
|
|
new \WP_Error( |
1219
|
|
|
'verify_secrets_incomplete', |
|
|
|
|
1220
|
|
|
__( 'Verification secrets are incomplete', 'jetpack' ), |
1221
|
|
|
400 |
1222
|
|
|
) |
1223
|
|
|
); |
1224
|
|
|
} elseif ( ! hash_equals( $secret_1, $stored_secrets['secret_1'] ) ) { |
1225
|
|
|
return $return_error( |
1226
|
|
|
new \WP_Error( |
1227
|
|
|
'verify_secrets_mismatch', |
|
|
|
|
1228
|
|
|
__( 'Secret mismatch', 'jetpack' ), |
1229
|
|
|
400 |
1230
|
|
|
) |
1231
|
|
|
); |
1232
|
|
|
} |
1233
|
|
|
|
1234
|
|
|
/** |
1235
|
|
|
* We've succeeded at verifying the previously generated secret. |
1236
|
|
|
* |
1237
|
|
|
* @since 7.5.0 |
1238
|
|
|
* |
1239
|
|
|
* @param string $action The type of secret to verify. |
1240
|
|
|
* @param \WP_User $user The user object. |
1241
|
|
|
*/ |
1242
|
|
|
do_action( 'jetpack_verify_secrets_success', $action, $user ); |
1243
|
|
|
|
1244
|
|
|
return $stored_secrets['secret_2']; |
1245
|
|
|
} |
1246
|
|
|
|
1247
|
|
|
/** |
1248
|
|
|
* Responds to a WordPress.com call to authorize the current user. |
1249
|
|
|
* Should be changed to protected. |
1250
|
|
|
*/ |
1251
|
|
|
public function handle_authorization() { |
1252
|
|
|
|
1253
|
|
|
} |
1254
|
|
|
|
1255
|
|
|
/** |
1256
|
|
|
* Builds a URL to the Jetpack connection auth page. |
1257
|
|
|
* This needs rethinking. |
1258
|
|
|
* |
1259
|
|
|
* @param bool $raw If true, URL will not be escaped. |
1260
|
|
|
* @param bool|string $redirect If true, will redirect back to Jetpack wp-admin landing page after connection. |
1261
|
|
|
* If string, will be a custom redirect. |
1262
|
|
|
* @param bool|string $from If not false, adds 'from=$from' param to the connect URL. |
1263
|
|
|
* @param bool $register If true, will generate a register URL regardless of the existing token, since 4.9.0. |
1264
|
|
|
* |
1265
|
|
|
* @return string Connect URL |
1266
|
|
|
*/ |
1267
|
|
|
public function build_connect_url( $raw, $redirect, $from, $register ) { |
1268
|
|
|
return array( $raw, $redirect, $from, $register ); |
1269
|
|
|
} |
1270
|
|
|
|
1271
|
|
|
/** |
1272
|
|
|
* Disconnects from the Jetpack servers. |
1273
|
|
|
* Forgets all connection details and tells the Jetpack servers to do the same. |
1274
|
|
|
*/ |
1275
|
|
|
public function disconnect_site() { |
1276
|
|
|
|
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
/** |
1280
|
|
|
* The Base64 Encoding of the SHA1 Hash of the Input. |
1281
|
|
|
* |
1282
|
|
|
* @param string $text The string to hash. |
1283
|
|
|
* @return string |
1284
|
|
|
*/ |
1285
|
|
|
public function sha1_base64( $text ) { |
1286
|
|
|
return base64_encode( sha1( $text, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
1287
|
|
|
} |
1288
|
|
|
|
1289
|
|
|
/** |
1290
|
|
|
* This function mirrors Jetpack_Data::is_usable_domain() in the WPCOM codebase. |
1291
|
|
|
* |
1292
|
|
|
* @param string $domain The domain to check. |
1293
|
|
|
* |
1294
|
|
|
* @return bool|WP_Error |
1295
|
|
|
*/ |
1296
|
|
|
public function is_usable_domain( $domain ) { |
1297
|
|
|
|
1298
|
|
|
// If it's empty, just fail out. |
1299
|
|
|
if ( ! $domain ) { |
1300
|
|
|
return new \WP_Error( |
1301
|
|
|
'fail_domain_empty', |
|
|
|
|
1302
|
|
|
/* translators: %1$s is a domain name. */ |
1303
|
|
|
sprintf( __( 'Domain `%1$s` just failed is_usable_domain check as it is empty.', 'jetpack' ), $domain ) |
1304
|
|
|
); |
1305
|
|
|
} |
1306
|
|
|
|
1307
|
|
|
/** |
1308
|
|
|
* Skips the usuable domain check when connecting a site. |
1309
|
|
|
* |
1310
|
|
|
* Allows site administrators with domains that fail gethostname-based checks to pass the request to WP.com |
1311
|
|
|
* |
1312
|
|
|
* @since 4.1.0 |
1313
|
|
|
* |
1314
|
|
|
* @param bool If the check should be skipped. Default false. |
1315
|
|
|
*/ |
1316
|
|
|
if ( apply_filters( 'jetpack_skip_usuable_domain_check', false ) ) { |
1317
|
|
|
return true; |
1318
|
|
|
} |
1319
|
|
|
|
1320
|
|
|
// None of the explicit localhosts. |
1321
|
|
|
$forbidden_domains = array( |
1322
|
|
|
'wordpress.com', |
1323
|
|
|
'localhost', |
1324
|
|
|
'localhost.localdomain', |
1325
|
|
|
'127.0.0.1', |
1326
|
|
|
'local.wordpress.test', // VVV pattern. |
1327
|
|
|
'local.wordpress-trunk.test', // VVV pattern. |
1328
|
|
|
'src.wordpress-develop.test', // VVV pattern. |
1329
|
|
|
'build.wordpress-develop.test', // VVV pattern. |
1330
|
|
|
); |
1331
|
|
View Code Duplication |
if ( in_array( $domain, $forbidden_domains, true ) ) { |
1332
|
|
|
return new \WP_Error( |
1333
|
|
|
'fail_domain_forbidden', |
|
|
|
|
1334
|
|
|
sprintf( |
1335
|
|
|
/* translators: %1$s is a domain name. */ |
1336
|
|
|
__( |
1337
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it is in the forbidden array.', |
1338
|
|
|
'jetpack' |
1339
|
|
|
), |
1340
|
|
|
$domain |
1341
|
|
|
) |
1342
|
|
|
); |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
// No .test or .local domains. |
1346
|
|
View Code Duplication |
if ( preg_match( '#\.(test|local)$#i', $domain ) ) { |
1347
|
|
|
return new \WP_Error( |
1348
|
|
|
'fail_domain_tld', |
|
|
|
|
1349
|
|
|
sprintf( |
1350
|
|
|
/* translators: %1$s is a domain name. */ |
1351
|
|
|
__( |
1352
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it uses an invalid top level domain.', |
1353
|
|
|
'jetpack' |
1354
|
|
|
), |
1355
|
|
|
$domain |
1356
|
|
|
) |
1357
|
|
|
); |
1358
|
|
|
} |
1359
|
|
|
|
1360
|
|
|
// No WPCOM subdomains. |
1361
|
|
View Code Duplication |
if ( preg_match( '#\.WordPress\.com$#i', $domain ) ) { |
1362
|
|
|
return new \WP_Error( |
1363
|
|
|
'fail_subdomain_wpcom', |
|
|
|
|
1364
|
|
|
sprintf( |
1365
|
|
|
/* translators: %1$s is a domain name. */ |
1366
|
|
|
__( |
1367
|
|
|
'Domain `%1$s` just failed is_usable_domain check as it is a subdomain of WordPress.com.', |
1368
|
|
|
'jetpack' |
1369
|
|
|
), |
1370
|
|
|
$domain |
1371
|
|
|
) |
1372
|
|
|
); |
1373
|
|
|
} |
1374
|
|
|
|
1375
|
|
|
// If PHP was compiled without support for the Filter module (very edge case). |
1376
|
|
|
if ( ! function_exists( 'filter_var' ) ) { |
1377
|
|
|
// Just pass back true for now, and let wpcom sort it out. |
1378
|
|
|
return true; |
1379
|
|
|
} |
1380
|
|
|
|
1381
|
|
|
return true; |
1382
|
|
|
} |
1383
|
|
|
|
1384
|
|
|
/** |
1385
|
|
|
* Gets the requested token. |
1386
|
|
|
* |
1387
|
|
|
* Tokens are one of two types: |
1388
|
|
|
* 1. Blog Tokens: These are the "main" tokens. Each site typically has one Blog Token, |
1389
|
|
|
* though some sites can have multiple "Special" Blog Tokens (see below). These tokens |
1390
|
|
|
* are not associated with a user account. They represent the site's connection with |
1391
|
|
|
* the Jetpack servers. |
1392
|
|
|
* 2. User Tokens: These are "sub-"tokens. Each connected user account has one User Token. |
1393
|
|
|
* |
1394
|
|
|
* All tokens look like "{$token_key}.{$private}". $token_key is a public ID for the |
1395
|
|
|
* token, and $private is a secret that should never be displayed anywhere or sent |
1396
|
|
|
* over the network; it's used only for signing things. |
1397
|
|
|
* |
1398
|
|
|
* Blog Tokens can be "Normal" or "Special". |
1399
|
|
|
* * Normal: The result of a normal connection flow. They look like |
1400
|
|
|
* "{$random_string_1}.{$random_string_2}" |
1401
|
|
|
* That is, $token_key and $private are both random strings. |
1402
|
|
|
* Sites only have one Normal Blog Token. Normal Tokens are found in either |
1403
|
|
|
* Jetpack_Options::get_option( 'blog_token' ) (usual) or the JETPACK_BLOG_TOKEN |
1404
|
|
|
* constant (rare). |
1405
|
|
|
* * Special: A connection token for sites that have gone through an alternative |
1406
|
|
|
* connection flow. They look like: |
1407
|
|
|
* ";{$special_id}{$special_version};{$wpcom_blog_id};.{$random_string}" |
1408
|
|
|
* That is, $private is a random string and $token_key has a special structure with |
1409
|
|
|
* lots of semicolons. |
1410
|
|
|
* Most sites have zero Special Blog Tokens. Special tokens are only found in the |
1411
|
|
|
* JETPACK_BLOG_TOKEN constant. |
1412
|
|
|
* |
1413
|
|
|
* In particular, note that Normal Blog Tokens never start with ";" and that |
1414
|
|
|
* Special Blog Tokens always do. |
1415
|
|
|
* |
1416
|
|
|
* When searching for a matching Blog Tokens, Blog Tokens are examined in the following |
1417
|
|
|
* order: |
1418
|
|
|
* 1. Defined Special Blog Tokens (via the JETPACK_BLOG_TOKEN constant) |
1419
|
|
|
* 2. Stored Normal Tokens (via Jetpack_Options::get_option( 'blog_token' )) |
1420
|
|
|
* 3. Defined Normal Tokens (via the JETPACK_BLOG_TOKEN constant) |
1421
|
|
|
* |
1422
|
|
|
* @param int|false $user_id false: Return the Blog Token. int: Return that user's User Token. |
1423
|
|
|
* @param string|false $token_key If provided, check that the token matches the provided input. |
1424
|
|
|
* @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. |
1425
|
|
|
* |
1426
|
|
|
* @return object|false |
1427
|
|
|
*/ |
1428
|
|
|
public function get_access_token( $user_id = false, $token_key = false, $suppress_errors = true ) { |
1429
|
|
|
$possible_special_tokens = array(); |
1430
|
|
|
$possible_normal_tokens = array(); |
1431
|
|
|
$user_tokens = \Jetpack_Options::get_option( 'user_tokens' ); |
1432
|
|
|
|
1433
|
|
|
if ( $user_id ) { |
|
|
|
|
1434
|
|
|
if ( ! $user_tokens ) { |
1435
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_user_tokens' ); |
|
|
|
|
1436
|
|
|
} |
1437
|
|
|
if ( self::JETPACK_MASTER_USER === $user_id ) { |
1438
|
|
|
$user_id = \Jetpack_Options::get_option( 'master_user' ); |
1439
|
|
|
if ( ! $user_id ) { |
1440
|
|
|
return $suppress_errors ? false : new \WP_Error( 'empty_master_user_option' ); |
|
|
|
|
1441
|
|
|
} |
1442
|
|
|
} |
1443
|
|
|
if ( ! isset( $user_tokens[ $user_id ] ) || ! $user_tokens[ $user_id ] ) { |
1444
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_token_for_user', sprintf( 'No token for user %d', $user_id ) ); |
|
|
|
|
1445
|
|
|
} |
1446
|
|
|
$user_token_chunks = explode( '.', $user_tokens[ $user_id ] ); |
1447
|
|
|
if ( empty( $user_token_chunks[1] ) || empty( $user_token_chunks[2] ) ) { |
1448
|
|
|
return $suppress_errors ? false : new \WP_Error( 'token_malformed', sprintf( 'Token for user %d is malformed', $user_id ) ); |
|
|
|
|
1449
|
|
|
} |
1450
|
|
|
if ( $user_token_chunks[2] !== (string) $user_id ) { |
1451
|
|
|
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] ) ); |
|
|
|
|
1452
|
|
|
} |
1453
|
|
|
$possible_normal_tokens[] = "{$user_token_chunks[0]}.{$user_token_chunks[1]}"; |
1454
|
|
|
} else { |
1455
|
|
|
$stored_blog_token = \Jetpack_Options::get_option( 'blog_token' ); |
1456
|
|
|
if ( $stored_blog_token ) { |
1457
|
|
|
$possible_normal_tokens[] = $stored_blog_token; |
1458
|
|
|
} |
1459
|
|
|
|
1460
|
|
|
$defined_tokens_string = Constants::get_constant( 'JETPACK_BLOG_TOKEN' ); |
1461
|
|
|
|
1462
|
|
|
if ( $defined_tokens_string ) { |
1463
|
|
|
$defined_tokens = explode( ',', $defined_tokens_string ); |
1464
|
|
|
foreach ( $defined_tokens as $defined_token ) { |
1465
|
|
|
if ( ';' === $defined_token[0] ) { |
1466
|
|
|
$possible_special_tokens[] = $defined_token; |
1467
|
|
|
} else { |
1468
|
|
|
$possible_normal_tokens[] = $defined_token; |
1469
|
|
|
} |
1470
|
|
|
} |
1471
|
|
|
} |
1472
|
|
|
} |
1473
|
|
|
|
1474
|
|
|
if ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { |
1475
|
|
|
$possible_tokens = $possible_normal_tokens; |
1476
|
|
|
} else { |
1477
|
|
|
$possible_tokens = array_merge( $possible_special_tokens, $possible_normal_tokens ); |
1478
|
|
|
} |
1479
|
|
|
|
1480
|
|
|
if ( ! $possible_tokens ) { |
|
|
|
|
1481
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_possible_tokens' ); |
|
|
|
|
1482
|
|
|
} |
1483
|
|
|
|
1484
|
|
|
$valid_token = false; |
1485
|
|
|
|
1486
|
|
|
if ( false === $token_key ) { |
1487
|
|
|
// Use first token. |
1488
|
|
|
$valid_token = $possible_tokens[0]; |
1489
|
|
|
} elseif ( self::MAGIC_NORMAL_TOKEN_KEY === $token_key ) { |
1490
|
|
|
// Use first normal token. |
1491
|
|
|
$valid_token = $possible_tokens[0]; // $possible_tokens only contains normal tokens because of earlier check. |
1492
|
|
|
} else { |
1493
|
|
|
// Use the token matching $token_key or false if none. |
1494
|
|
|
// Ensure we check the full key. |
1495
|
|
|
$token_check = rtrim( $token_key, '.' ) . '.'; |
1496
|
|
|
|
1497
|
|
|
foreach ( $possible_tokens as $possible_token ) { |
1498
|
|
|
if ( hash_equals( substr( $possible_token, 0, strlen( $token_check ) ), $token_check ) ) { |
1499
|
|
|
$valid_token = $possible_token; |
1500
|
|
|
break; |
1501
|
|
|
} |
1502
|
|
|
} |
1503
|
|
|
} |
1504
|
|
|
|
1505
|
|
|
if ( ! $valid_token ) { |
1506
|
|
|
return $suppress_errors ? false : new \WP_Error( 'no_valid_token' ); |
|
|
|
|
1507
|
|
|
} |
1508
|
|
|
|
1509
|
|
|
return (object) array( |
1510
|
|
|
'secret' => $valid_token, |
1511
|
|
|
'external_user_id' => (int) $user_id, |
1512
|
|
|
); |
1513
|
|
|
} |
1514
|
|
|
|
1515
|
|
|
/** |
1516
|
|
|
* In some setups, $HTTP_RAW_POST_DATA can be emptied during some IXR_Server paths |
1517
|
|
|
* since it is passed by reference to various methods. |
1518
|
|
|
* Capture it here so we can verify the signature later. |
1519
|
|
|
* |
1520
|
|
|
* @param Array $methods an array of available XMLRPC methods. |
1521
|
|
|
* @return Array the same array, since this method doesn't add or remove anything. |
1522
|
|
|
*/ |
1523
|
|
|
public function xmlrpc_methods( $methods ) { |
1524
|
|
|
$this->raw_post_data = $GLOBALS['HTTP_RAW_POST_DATA']; |
1525
|
|
|
return $methods; |
1526
|
|
|
} |
1527
|
|
|
|
1528
|
|
|
/** |
1529
|
|
|
* Resets the raw post data parameter for testing purposes. |
1530
|
|
|
*/ |
1531
|
|
|
public function reset_raw_post_data() { |
1532
|
|
|
$this->raw_post_data = null; |
1533
|
|
|
} |
1534
|
|
|
|
1535
|
|
|
/** |
1536
|
|
|
* Registering an additional method. |
1537
|
|
|
* |
1538
|
|
|
* @param Array $methods an array of available XMLRPC methods. |
1539
|
|
|
* @return Array the amended array in case the method is added. |
1540
|
|
|
*/ |
1541
|
|
|
public function public_xmlrpc_methods( $methods ) { |
1542
|
|
|
if ( array_key_exists( 'wp.getOptions', $methods ) ) { |
1543
|
|
|
$methods['wp.getOptions'] = array( $this, 'jetpack_getOptions' ); |
1544
|
|
|
} |
1545
|
|
|
return $methods; |
1546
|
|
|
} |
1547
|
|
|
|
1548
|
|
|
/** |
1549
|
|
|
* Handles a getOptions XMLRPC method call. |
1550
|
|
|
* |
1551
|
|
|
* @param Array $args method call arguments. |
1552
|
|
|
* @return an amended XMLRPC server options array. |
1553
|
|
|
*/ |
1554
|
|
|
public function jetpack_getOptions( $args ) { |
1555
|
|
|
global $wp_xmlrpc_server; |
1556
|
|
|
|
1557
|
|
|
$wp_xmlrpc_server->escape( $args ); |
1558
|
|
|
|
1559
|
|
|
$username = $args[1]; |
1560
|
|
|
$password = $args[2]; |
1561
|
|
|
|
1562
|
|
|
$user = $wp_xmlrpc_server->login( $username, $password ); |
1563
|
|
|
if ( ! $user ) { |
1564
|
|
|
return $wp_xmlrpc_server->error; |
1565
|
|
|
} |
1566
|
|
|
|
1567
|
|
|
$options = array(); |
1568
|
|
|
$user_data = $this->get_connected_user_data(); |
1569
|
|
|
if ( is_array( $user_data ) ) { |
1570
|
|
|
$options['jetpack_user_id'] = array( |
1571
|
|
|
'desc' => __( 'The WP.com user ID of the connected user', 'jetpack' ), |
1572
|
|
|
'readonly' => true, |
1573
|
|
|
'value' => $user_data['ID'], |
1574
|
|
|
); |
1575
|
|
|
$options['jetpack_user_login'] = array( |
1576
|
|
|
'desc' => __( 'The WP.com username of the connected user', 'jetpack' ), |
1577
|
|
|
'readonly' => true, |
1578
|
|
|
'value' => $user_data['login'], |
1579
|
|
|
); |
1580
|
|
|
$options['jetpack_user_email'] = array( |
1581
|
|
|
'desc' => __( 'The WP.com user email of the connected user', 'jetpack' ), |
1582
|
|
|
'readonly' => true, |
1583
|
|
|
'value' => $user_data['email'], |
1584
|
|
|
); |
1585
|
|
|
$options['jetpack_user_site_count'] = array( |
1586
|
|
|
'desc' => __( 'The number of sites of the connected WP.com user', 'jetpack' ), |
1587
|
|
|
'readonly' => true, |
1588
|
|
|
'value' => $user_data['site_count'], |
1589
|
|
|
); |
1590
|
|
|
} |
1591
|
|
|
$wp_xmlrpc_server->blog_options = array_merge( $wp_xmlrpc_server->blog_options, $options ); |
1592
|
|
|
$args = stripslashes_deep( $args ); |
1593
|
|
|
return $wp_xmlrpc_server->wp_getOptions( $args ); |
1594
|
|
|
} |
1595
|
|
|
|
1596
|
|
|
/** |
1597
|
|
|
* Adds Jetpack-specific options to the output of the XMLRPC options method. |
1598
|
|
|
* |
1599
|
|
|
* @param Array $options standard Core options. |
1600
|
|
|
* @return Array amended options. |
1601
|
|
|
*/ |
1602
|
|
|
public function xmlrpc_options( $options ) { |
1603
|
|
|
$jetpack_client_id = false; |
1604
|
|
|
if ( $this->is_active() ) { |
1605
|
|
|
$jetpack_client_id = \Jetpack_Options::get_option( 'id' ); |
1606
|
|
|
} |
1607
|
|
|
$options['jetpack_version'] = array( |
1608
|
|
|
'desc' => __( 'Jetpack Plugin Version', 'jetpack' ), |
1609
|
|
|
'readonly' => true, |
1610
|
|
|
'value' => Constants::get_constant( 'JETPACK__VERSION' ), |
1611
|
|
|
); |
1612
|
|
|
|
1613
|
|
|
$options['jetpack_client_id'] = array( |
1614
|
|
|
'desc' => __( 'The Client ID/WP.com Blog ID of this site', 'jetpack' ), |
1615
|
|
|
'readonly' => true, |
1616
|
|
|
'value' => $jetpack_client_id, |
1617
|
|
|
); |
1618
|
|
|
return $options; |
1619
|
|
|
} |
1620
|
|
|
|
1621
|
|
|
/** |
1622
|
|
|
* Resets the saved authentication state in between testing requests. |
1623
|
|
|
*/ |
1624
|
|
|
public function reset_saved_auth_state() { |
1625
|
|
|
$this->xmlrpc_verification = null; |
1626
|
|
|
} |
1627
|
|
|
} |
1628
|
|
|
|
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.