Completed
Push — add/xmlrpc-user-api ( d35894...e9f504 )
by
unknown
467:32 queued 460:49
created

class.jetpack-xmlrpc-server.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use Automattic\Jetpack\Tracking;
4
/**
5
 * Just a sack of functions.  Not actually an IXR_Server
6
 */
7
class Jetpack_XMLRPC_Server {
8
	/**
9
	 * The current error object
10
	 */
11
	public $error = null;
12
13
	/**
14
	 * The current user
15
	 */
16
	public $user = null;
17
18
	/**
19
	 * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
20
	 * user is not authenticated (->login()) then the methods are never added,
21
	 * so they will get a "does not exist" error.
22
	 */
23
	function xmlrpc_methods( $core_methods ) {
24
		$jetpack_methods = array(
25
			'jetpack.jsonAPI'           => array( $this, 'json_api' ),
26
			'jetpack.verifyAction'      => array( $this, 'verify_action' ),
27
			'jetpack.getUser'           => array( $this, 'get_user' ),
28
			'jetpack.remoteRegister'    => array( $this, 'remote_register' ),
29
			'jetpack.remoteProvision'   => array( $this, 'remote_provision' ),
30
		);
31
32
		$this->user = $this->login();
33
34
		if ( $this->user ) {
35
			$jetpack_methods = array_merge( $jetpack_methods, array(
36
				'jetpack.testConnection'    => array( $this, 'test_connection' ),
37
				'jetpack.testAPIUserCode'   => array( $this, 'test_api_user_code' ),
38
				'jetpack.featuresAvailable' => array( $this, 'features_available' ),
39
				'jetpack.featuresEnabled'   => array( $this, 'features_enabled' ),
40
				'jetpack.disconnectBlog'    => array( $this, 'disconnect_blog' ),
41
				'jetpack.unlinkUser'        => array( $this, 'unlink_user' ),
42
				'jetpack.syncObject'        => array( $this, 'sync_object' ),
43
				'jetpack.idcUrlValidation'  => array( $this, 'validate_urls_for_idc_mitigation' ),
44
			) );
45
46
			if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
47
				$jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject'];
48
				$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
49
			}
50
51
			/**
52
			 * Filters the XML-RPC methods available to Jetpack for authenticated users.
53
			 *
54
			 * @since 1.1.0
55
			 *
56
			 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
57
			 * @param array $core_methods Available core XML-RPC methods.
58
			 * @param WP_User $user Information about a given WordPress user.
59
			 */
60
			$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
61
		}
62
63
		/**
64
		 * Filters the XML-RPC methods available to Jetpack for unauthenticated users.
65
		 *
66
		 * @since 3.0.0
67
		 *
68
		 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
69
		 * @param array $core_methods Available core XML-RPC methods.
70
		 */
71
		return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods );
72
	}
73
74
	/**
75
	 * Whitelist of the bootstrap XML-RPC methods
76
	 */
77
	function bootstrap_xmlrpc_methods() {
78
		return array(
79
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
80
			'jetpack.remoteRegister' => array( $this, 'remote_register' ),
81
		);
82
	}
83
84
	function authorize_xmlrpc_methods() {
85
		return array(
86
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
87
		);
88
	}
89
90
	function provision_xmlrpc_methods() {
91
		return array(
92
			'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
93
			'jetpack.remoteProvision' => array( $this, 'remote_provision' ),
94
			'jetpack.remoteConnect'   => array( $this, 'remote_connect' ),
95
			'jetpack.getUser'         => array( $this, 'get_user' ),
96
		);
97
	}
98
99
	/**
100
	 * Used to verify whether a local user exists and what role they have.
101
	 *
102
	 * @param int|string $user_id The local User's ID, username, or email address.
103
	 *
104
	 * @return array|IXR_Error Information about the user, or error if no such user found:
105
	 *                         roles:     string[] The user's rols.
106
	 *                         login:     string   The user's username.
107
	 *                         email_hash string[] The MD5 hash of the user's normalized email address.
108
	 *                         caps       string[] The user's capabilities.
109
	 *                         allcaps    string[] The user's granular capabilities, merged from role capabilities.
110
	 *                         token_key  string   The Token Key of the user's Jetpack token. Empty string if none.
111
	 */
112
	function get_user( $user_id ) {
113
		if ( ! $user_id ) {
114
			return $this->error(
115
				new Jetpack_Error(
116
					'invalid_user',
117
					__( 'Invalid user identifier.', 'jetpack' ),
118
					400
119
				),
120
				'jpc_get_user_fail'
121
			);
122
		}
123
124
		$user = $this->get_user_by_anything( $user_id );
125
126
		if ( ! $user ) {
127
			return $this->error(
128
				new Jetpack_Error(
129
					'user_unknown',
130
					__( 'User not found.', 'jetpack' ),
131
					404
132
				),
133
				'jpc_get_user_fail'
134
			);
135
		}
136
137
		$connection = Jetpack::connection();
138
		$user_token = $connection->get_access_token( $user->ID );
139
140
		if ( $user_token ) {
141
			list( $user_token_key, $user_token_private ) = explode( '.', $user_token->secret );
0 ignored issues
show
The assignment to $user_token_private is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
142
			if ( $user_token_key === $user_token->secret ) {
143
				$user_token_key = '';
144
			}
145
		} else {
146
			$user_token_key = '';
147
		}
148
149
		return array(
150
			'id'         => $user->ID,
151
			'login'      => $user->user_login,
152
			'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
153
			'roles'      => $user->roles,
154
			'caps'       => $user->caps,
155
			'allcaps'    => $user->allcaps,
156
			'token_key'  => $user_token_key,
157
		);
158
	}
159
160
	function remote_authorize( $request ) {
161
		$user = get_user_by( 'id', $request['state'] );
162
		Tracking::record_user_event( 'jpc_remote_authorize_begin', array(), $user );
163
164
		foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
165
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
166
				return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), 'jpc_remote_authorize_fail' );
167
			}
168
		}
169
170
		if ( ! $user ) {
171
			return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ), 'jpc_remote_authorize_fail' );
172
		}
173
174
		if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
175
			return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ), 'jpc_remote_authorize_fail' );
176
		}
177
178
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
179
180
		if ( is_a( $verified, 'IXR_Error' ) ) {
181
			return $this->error( $verified, 'jpc_remote_authorize_fail' );
182
		}
183
184
		wp_set_current_user( $request['state'] );
185
186
		$client_server = new Jetpack_Client_Server;
187
		$result = $client_server->authorize( $request );
188
189
		if ( is_wp_error( $result ) ) {
190
			return $this->error( $result, 'jpc_remote_authorize_fail' );
191
		}
192
193
		Tracking::record_user_event( 'jpc_remote_authorize_success' );
194
195
		return array(
196
			'result' => $result,
197
		);
198
	}
199
200
	/**
201
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
202
	 * register this site so that a plan can be provisioned.
203
	 *
204
	 * @param array $request An array containing at minimum nonce and local_user keys.
205
	 *
206
	 * @return WP_Error|array
207
	 */
208
	public function remote_register( $request ) {
209
		Tracking::record_user_event( 'jpc_remote_register_begin', array() );
210
211
		$user = $this->fetch_and_verify_local_user( $request );
212
213
		if ( ! $user ) {
214
			return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_register_fail' );
215
		}
216
217
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
218
			return $this->error( $user, 'jpc_remote_register_fail' );
219
		}
220
221
		if ( empty( $request['nonce'] ) ) {
222
			return $this->error(
223
				new Jetpack_Error(
224
					'nonce_missing',
225
					__( 'The required "nonce" parameter is missing.', 'jetpack' ),
226
					400
227
				),
228
				'jpc_remote_register_fail'
229
			);
230
		}
231
232
		$nonce = sanitize_text_field( $request['nonce'] );
233
		unset( $request['nonce'] );
234
235
		$api_url  = Jetpack::fix_url_for_bad_hosts( Jetpack::api_url( 'partner_provision_nonce_check' ) );
236
		$response = Jetpack_Client::_wp_remote_request(
237
			esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
238
			array( 'method' => 'GET' ),
239
			true
240
		);
241
242
		if (
243
			200 !== wp_remote_retrieve_response_code( $response ) ||
244
			'OK' !== trim( wp_remote_retrieve_body( $response ) )
245
		) {
246
			return $this->error(
247
				new Jetpack_Error(
248
					'invalid_nonce',
249
					__( 'There was an issue validating this request.', 'jetpack' ),
250
					400
251
				),
252
				'jpc_remote_register_fail'
253
			);
254
		}
255
256
		if ( ! Jetpack_Options::get_option( 'id' ) || ! Jetpack_Data::get_access_token() || ! empty( $request['force'] ) ) {
257
			wp_set_current_user( $user->ID );
258
259
			// This code mostly copied from Jetpack::admin_page_load.
260
			Jetpack::maybe_set_version_option();
261
			$registered = Jetpack::try_registration();
262
			if ( is_wp_error( $registered ) ) {
263
				return $this->error( $registered, 'jpc_remote_register_fail' );
264
			} elseif ( ! $registered ) {
265
				return $this->error(
266
					new Jetpack_Error(
267
						'registration_error',
268
						__( 'There was an unspecified error registering the site', 'jetpack' ),
269
						400
270
					),
271
					'jpc_remote_register_fail'
272
				);
273
			}
274
		}
275
276
		Tracking::record_user_event( 'jpc_remote_register_success' );
277
278
		return array(
279
			'client_id' => Jetpack_Options::get_option( 'id' )
280
		);
281
	}
282
283
	/**
284
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
285
	 * register this site so that a plan can be provisioned.
286
	 *
287
	 * @param array $request An array containing at minimum a nonce key and a local_username key.
288
	 *
289
	 * @return WP_Error|array
290
	 */
291
	public function remote_provision( $request ) {
292
		$user = $this->fetch_and_verify_local_user( $request );
293
294
		if ( ! $user ) {
295
			return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_provision_fail' );
296
		}
297
298
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
299
			return $this->error( $user, 'jpc_remote_provision_fail' );
300
		}
301
302
		$site_icon = get_site_icon_url();
303
304
		$auto_enable_sso = ( ! Jetpack::is_active() || Jetpack::is_module_active( 'sso' ) );
305
306
		/** This filter is documented in class.jetpack-cli.php */
307 View Code Duplication
		if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) {
308
			$redirect_uri = add_query_arg(
309
				array(
310
					'action'      => 'jetpack-sso',
311
					'redirect_to' => rawurlencode( admin_url() ),
312
				),
313
				wp_login_url() // TODO: come back to Jetpack dashboard?
314
			);
315
		} else {
316
			$redirect_uri = admin_url();
317
		}
318
319
		// Generate secrets.
320
		$role    = Jetpack::translate_user_to_role( $user );
321
		$secrets = Jetpack::init()->generate_secrets( 'authorize', $user->ID );
322
323
		$response = array(
324
			'jp_version'   => JETPACK__VERSION,
325
			'redirect_uri' => $redirect_uri,
326
			'user_id'      => $user->ID,
327
			'user_email'   => $user->user_email,
328
			'user_login'   => $user->user_login,
329
			'scope'        => Jetpack::sign_role( $role, $user->ID ),
330
			'secret'       => $secrets['secret_1'],
331
			'is_active'    => Jetpack::is_active(),
332
		);
333
334
		if ( $site_icon ) {
335
			$response['site_icon'] = $site_icon;
336
		}
337
338
		if ( ! empty( $request['onboarding'] ) ) {
339
			Jetpack::create_onboarding_token();
340
			$response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' );
341
		}
342
343
		return $response;
344
	}
345
346
	/**
347
	 * Given an array containing a local user identifier and a nonce, will attempt to fetch and set
348
	 * an access token for the given user.
349
	 *
350
	 * @param array $request An array containing local_user and nonce keys at minimum.
351
	 * @return mixed
352
	 */
353
	public function remote_connect( $request, $ixr_client = false ) {
354
		if ( Jetpack::is_active() ) {
355
			return $this->error(
356
				new WP_Error(
357
					'already_connected',
358
					__( 'Jetpack is already connected.', 'jetpack' ),
359
					400
360
				),
361
				'jpc_remote_connect_fail'
362
			);
363
		}
364
365
		$user = $this->fetch_and_verify_local_user( $request );
366
367
		if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
368
			return $this->error(
369
				new WP_Error(
370
					'input_error',
371
					__( 'Valid user is required.', 'jetpack' ),
372
					400
373
				),
374
				'jpc_remote_connect_fail'
375
			);
376
		}
377
378
		if ( empty( $request['nonce'] ) ) {
379
			return $this->error(
380
				new WP_Error(
381
					'input_error',
382
					__( 'A non-empty nonce must be supplied.', 'jetpack' ),
383
					400
384
				),
385
				'jpc_remote_connect_fail'
386
			);
387
		}
388
389
		if ( ! $ixr_client ) {
390
			Jetpack::load_xml_rpc_client();
391
			$ixr_client = new Jetpack_IXR_Client();
392
		}
393
		$ixr_client->query( 'jetpack.getUserAccessToken', array(
394
			'nonce'            => sanitize_text_field( $request['nonce'] ),
395
			'external_user_id' => $user->ID,
396
		) );
397
398
		$token = $ixr_client->isError() ? false : $ixr_client->getResponse();
399
		if ( empty( $token ) ) {
400
			return $this->error(
401
				new WP_Error(
402
					'token_fetch_failed',
403
					__( 'Failed to fetch user token from WordPress.com.', 'jetpack' ),
404
					400
405
				),
406
				'jpc_remote_connect_fail'
407
			);
408
		}
409
		$token = sanitize_text_field( $token );
410
411
		Jetpack::update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );
412
413
		$this->do_post_authorization();
414
415
		return Jetpack::is_active();
416
	}
417
418
	private function fetch_and_verify_local_user( $request ) {
419
		if ( empty( $request['local_user'] ) ) {
420
			return $this->error(
421
				new Jetpack_Error(
422
					'local_user_missing',
423
					__( 'The required "local_user" parameter is missing.', 'jetpack' ),
424
					400
425
				),
426
				'jpc_remote_provision_fail'
427
			);
428
		}
429
430
		// local user is used to look up by login, email or ID
431
		$local_user_info = $request['local_user'];
432
433
		return $this->get_user_by_anything( $local_user_info );
434
	}
435
436
	private function get_user_by_anything( $user_id ) {
437
		$user = get_user_by( 'login', $user_id );
438
439
		if ( ! $user ) {
440
			$user = get_user_by( 'email', $user_id );
441
		}
442
443
		if ( ! $user ) {
444
			$user = get_user_by( 'ID', $user_id );
445
		}
446
447
		return $user;
448
	}
449
450
	private function tracks_record_error( $name, $error, $user = null ) {
451
		if ( is_wp_error( $error ) ) {
452
			Tracking::record_user_event( $name, array(
453
				'error_code' => $error->get_error_code(),
454
				'error_message' => $error->get_error_message()
455
			), $user );
456
		} elseif( is_a( $error, 'IXR_Error' ) ) {
457
			Tracking::record_user_event( $name, array(
458
				'error_code' => $error->code,
459
				'error_message' => $error->message
460
			), $user );
461
		}
462
463
		return $error;
464
	}
465
466
	/**
467
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
468
	 *
469
	 * Possible error_codes:
470
	 *
471
	 * verify_secret_1_missing
472
	 * verify_secret_1_malformed
473
	 * verify_secrets_missing: verification secrets are not found in database
474
	 * verify_secrets_incomplete: verification secrets are only partially found in database
475
	 * verify_secrets_expired: verification secrets have expired
476
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
477
	 * state_missing: required parameter of state not found
478
	 * state_malformed: state is not a digit
479
	 * invalid_state: state in request does not match the stored state
480
	 *
481
	 * The 'authorize' and 'register' actions have additional error codes
482
	 *
483
	 * Possible values for action are `authorize`, `publicize` and `register`.
484
	 *
485
	 * state_missing: a state ( user id ) was not supplied
486
	 * state_malformed: state is not the correct data type
487
	 * invalid_state: supplied state does not match the stored state
488
	 */
489
	function verify_action( $params ) {
490
		$action = $params[0];
491
		$verify_secret = $params[1];
492
		$state = isset( $params[2] ) ? $params[2] : '';
493
		$user = get_user_by( 'id', $state );
494
		$tracks_failure_event_name = '';
495
496
		if ( 'authorize' === $action ) {
497
			$tracks_failure_event_name = 'jpc_verify_authorize_fail';
498
			Tracking::record_user_event( 'jpc_verify_authorize_begin', array(), $user );
499
		}
500
		if ( 'publicize' === $action ) {
501
			// This action is used on a response from a direct XML-RPC done from WordPress.com
502
			$tracks_failure_event_name = 'jpc_verify_publicize_fail';
503
			Tracking::record_user_event( 'jpc_verify_publicize_begin', array(), $user );
504
		}
505
		if ( 'register' === $action ) {
506
			$tracks_failure_event_name = 'jpc_verify_register_fail';
507
			Tracking::record_user_event( 'jpc_verify_register_begin', array(), $user );
508
		}
509
510
		if ( empty( $verify_secret ) ) {
511
			return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ), $tracks_failure_event_name, $user );
512
		} else if ( ! is_string( $verify_secret ) ) {
513
			return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ), $tracks_failure_event_name, $user );
514
		} else if ( empty( $state ) ) {
515
			return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ), $tracks_failure_event_name, $user );
516
		} else if ( ! ctype_digit( $state ) ) {
517
			return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ), $tracks_failure_event_name, $user );
518
		}
519
520
		$secrets = Jetpack::get_secrets( $action, $state );
521
522
		if ( ! $secrets ) {
523
			Jetpack::delete_secrets( $action, $state );
524
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification secrets not found', 400 ), $tracks_failure_event_name, $user );
525
		}
526
527
		if ( is_wp_error( $secrets ) ) {
528
			Jetpack::delete_secrets( $action, $state );
529
			return $this->error( new Jetpack_Error( $secrets->get_error_code(), $secrets->get_error_message(), 400 ), $tracks_failure_event_name, $user );
530
		}
531
532
		if ( empty( $secrets['secret_1'] ) || empty( $secrets['secret_2'] ) || empty( $secrets['exp'] ) ) {
533
			Jetpack::delete_secrets( $action, $state );
534
			return $this->error( new Jetpack_Error( 'verify_secrets_incomplete', 'Verification secrets are incomplete', 400 ), $tracks_failure_event_name, $user );
535
		}
536
537
		if ( ! hash_equals( $verify_secret, $secrets['secret_1'] ) ) {
538
			Jetpack::delete_secrets( $action, $state );
539
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ), $tracks_failure_event_name, $user );
540
		}
541
542
		Jetpack::delete_secrets( $action, $state );
543
544
		if ( 'authorize' === $action ) {
545
			Tracking::record_user_event( 'jpc_verify_authorize_success', array(), $user );
546
		}
547
		if ( 'publicize' === $action ) {
548
			Tracking::record_user_event( 'jpc_verify_publicize_success', array(), $user );
549
		}
550
		if ( 'register' === $action ) {
551
			Tracking::record_user_event( 'jpc_verify_register_success', array(), $user );
552
		}
553
554
		return $secrets['secret_2'];
555
	}
556
557
	/**
558
	 * Wrapper for wp_authenticate( $username, $password );
559
	 *
560
	 * @return WP_User|bool
561
	 */
562
	function login() {
563
		Jetpack::init()->require_jetpack_authentication();
564
		$user = wp_authenticate( 'username', 'password' );
565
		if ( is_wp_error( $user ) ) {
566
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
567
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
568
			} else {
569
				$this->error = $user;
570
			}
571
			return false;
572
		} else if ( !$user ) { // Shouldn't happen.
573
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
574
			return false;
575
		}
576
577
		return $user;
578
	}
579
580
	/**
581
	 * Returns the current error as an IXR_Error
582
	 *
583
	 * @return bool|IXR_Error
584
	 */
585
	function error( $error = null, $tracks_event_name = null, $user = null ) {
586
		// record using Tracks
587
		if ( null !== $tracks_event_name ) {
588
			$this->tracks_record_error( $tracks_event_name, $error, $user );
589
		}
590
591
		if ( !is_null( $error ) ) {
592
			$this->error = $error;
593
		}
594
595
		if ( is_wp_error( $this->error ) ) {
596
			$code = $this->error->get_error_data();
597
			if ( !$code ) {
598
				$code = -10520;
599
			}
600
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
601
			return new IXR_Error( $code, $message );
602
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
603
			return $this->error;
604
		}
605
606
		return false;
607
	}
608
609
/* API Methods */
610
611
	/**
612
	 * Just authenticates with the given Jetpack credentials.
613
	 *
614
	 * @return string The current Jetpack version number
615
	 */
616
	function test_connection() {
617
		return JETPACK__VERSION;
618
	}
619
620
	function test_api_user_code( $args ) {
621
		$client_id = (int) $args[0];
622
		$user_id   = (int) $args[1];
623
		$nonce     = (string) $args[2];
624
		$verify    = (string) $args[3];
625
626
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
627
			return false;
628
		}
629
630
		$user = get_user_by( 'id', $user_id );
631
		if ( !$user || is_wp_error( $user ) ) {
632
			return false;
633
		}
634
635
		/* debugging
636
		error_log( "CLIENT: $client_id" );
637
		error_log( "USER:   $user_id" );
638
		error_log( "NONCE:  $nonce" );
639
		error_log( "VERIFY: $verify" );
640
		*/
641
642
		$jetpack_token = Jetpack_Data::get_access_token( $user_id );
643
644
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
645
		if ( !$api_user_code ) {
646
			return false;
647
		}
648
649
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
650
			'client_id' => (int) $client_id,
651
			'user_id'   => (int) $user_id,
652
			'nonce'     => (string) $nonce,
653
			'code'      => (string) $api_user_code,
654
		) ), $jetpack_token->secret );
655
656
		if ( ! hash_equals( $hmac, $verify ) ) {
657
			return false;
658
		}
659
660
		return $user_id;
661
	}
662
663
	/**
664
	* Disconnect this blog from the connected wordpress.com account
665
	* @return boolean
666
	*/
667
	function disconnect_blog() {
668
669
		// For tracking
670
		if ( ! empty( $this->user->ID ) ) {
671
			wp_set_current_user( $this->user->ID );
672
		}
673
674
		Jetpack::log( 'disconnect' );
675
		Jetpack::disconnect();
676
677
		return true;
678
	}
679
680
	/**
681
	 * Unlink a user from WordPress.com
682
	 *
683
	 * This will fail if called by the Master User.
684
	 */
685
	function unlink_user() {
686
		Jetpack::log( 'unlink' );
687
		return Jetpack::unlink_user();
688
	}
689
690
	/**
691
	 * Returns any object that is able to be synced
692
	 */
693
	function sync_object( $args ) {
694
		// e.g. posts, post, 5
695
		list( $module_name, $object_type, $id ) = $args;
696
697
		$sync_module = Jetpack_Sync_Modules::get_module( $module_name );
698
		$codec = Jetpack_Sync_Sender::get_instance()->get_codec();
699
700
		return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
701
	}
702
703
	/**
704
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
705
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
706
	 * and the remote Jetpack site.
707
	 *
708
	 * @return array
709
	 */
710
	function validate_urls_for_idc_mitigation() {
711
		return array(
712
			'home'    => Jetpack_Sync_Functions::home_url(),
713
			'siteurl' => Jetpack_Sync_Functions::site_url(),
714
		);
715
	}
716
717
	/**
718
	 * Returns what features are available. Uses the slug of the module files.
719
	 *
720
	 * @return array
721
	 */
722 View Code Duplication
	function features_available() {
723
		$raw_modules = Jetpack::get_available_modules();
724
		$modules = array();
725
		foreach ( $raw_modules as $module ) {
726
			$modules[] = Jetpack::get_module_slug( $module );
727
		}
728
729
		return $modules;
730
	}
731
732
	/**
733
	 * Returns what features are enabled. Uses the slug of the modules files.
734
	 *
735
	 * @return array
736
	 */
737 View Code Duplication
	function features_enabled() {
738
		$raw_modules = Jetpack::get_active_modules();
739
		$modules = array();
740
		foreach ( $raw_modules as $module ) {
741
			$modules[] = Jetpack::get_module_slug( $module );
742
		}
743
744
		return $modules;
745
	}
746
747
	function update_attachment_parent( $args ) {
748
		$attachment_id = (int) $args[0];
749
		$parent_id     = (int) $args[1];
750
751
		return wp_update_post( array(
752
			'ID'          => $attachment_id,
753
			'post_parent' => $parent_id,
754
		) );
755
	}
756
757
	function json_api( $args = array() ) {
758
		$json_api_args = $args[0];
759
		$verify_api_user_args = $args[1];
760
761
		$method       = (string) $json_api_args[0];
762
		$url          = (string) $json_api_args[1];
763
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
764
		$user_details = (array) $json_api_args[4];
765
		$locale       = (string) $json_api_args[5];
766
767
		if ( !$verify_api_user_args ) {
768
			$user_id = 0;
769
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
770
			$user_id = (int) $verify_api_user_args[1];
771
			if ( $user_id ) {
772
				$user = get_user_by( 'id', $user_id );
773
				if ( !$user || is_wp_error( $user ) ) {
774
					return false;
775
				}
776
			}
777
		} else {
778
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
779
			if ( !$user_id ) {
780
				return false;
781
			}
782
		}
783
784
		/* debugging
785
		error_log( "-- begin json api via jetpack debugging -- " );
786
		error_log( "METHOD: $method" );
787
		error_log( "URL: $url" );
788
		error_log( "POST BODY: $post_body" );
789
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
790
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
791
		error_log( "-- end json api via jetpack debugging -- " );
792
		*/
793
794
		if ( 'en' !== $locale ) {
795
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
796
			$new_locale = $locale;
797
			if ( strpos( $locale, '-' ) !== false ) {
798
				$locale_pieces = explode( '-', $locale );
799
				$new_locale = $locale_pieces[0];
800
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
801
			} else {
802
				// .com might pass 'fr' because thats what our language files are named as, where core seems
803
				// to do fr_FR - so try that if we don't think we can load the file.
804
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
805
					$new_locale =  $locale . '_' . strtoupper( $locale );
806
				}
807
			}
808
809
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
810
				unload_textdomain( 'default' );
811
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
812
			}
813
		}
814
815
		$old_user = wp_get_current_user();
816
		wp_set_current_user( $user_id );
817
818
		if ( $user_id ) {
819
			$token_key = false;
820
		} else {
821
			$jetpack   = Jetpack::init();
822
			$verified  = $jetpack->verify_xml_rpc_signature();
823
			$token_key = $verified['token_key'];
824
		}
825
826
		$token = Jetpack_Data::get_access_token( $user_id, $token_key );
827
		if ( !$token || is_wp_error( $token ) ) {
828
			return false;
829
		}
830
831
		define( 'REST_API_REQUEST', true );
832
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
833
834
		// needed?
835
		require_once ABSPATH . 'wp-admin/includes/admin.php';
836
837
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
838
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
839
		$api->token_details['user'] = $user_details;
840
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
841
842
		$display_errors = ini_set( 'display_errors', 0 );
843
		ob_start();
844
		$content_type = $api->serve( false );
845
		$output = ob_get_clean();
846
		ini_set( 'display_errors', $display_errors );
847
848
		$nonce = wp_generate_password( 10, false );
849
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
850
851
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
852
853
		return array(
854
			(string) $output,
855
			(string) $nonce,
856
			(string) $hmac,
857
		);
858
	}
859
860
	/**
861
	 * Handles authorization actions after connecting a site, such as enabling modules.
862
	 *
863
	 * This do_post_authorization() is used in this class, as opposed to calling
864
	 * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary.
865
	 *
866
	 * @return void
867
	 */
868
	public function do_post_authorization() {
869
		/** This filter is documented in class.jetpack-cli.php */
870
		$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
871
		Jetpack::handle_post_authorization_actions( $enable_sso, false, false );
872
	}
873
}
874