Completed
Push — memberships/widget ( e36f77...e31ee1 )
by
unknown
48:35 queued 32:12
created

class.jetpack-xmlrpc-server.php (29 issues)

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

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
131
					__( 'Invalid user identifier.', 'jetpack' ),
132
					400
133
				),
134
				'jpc_get_user_fail'
135
			);
136
		}
137
138
		$user = $this->get_user_by_anything( $user_id );
139
140
		if ( ! $user ) {
141
			return $this->error(
142
				new Jetpack_Error(
143
					'user_unknown',
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'user_unknown'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
144
					__( 'User not found.', 'jetpack' ),
145
					404
146
				),
147
				'jpc_get_user_fail'
148
			);
149
		}
150
151
		$connection = Jetpack::connection();
152
		$user_token = $connection->get_access_token( $user->ID );
153
154
		if ( $user_token ) {
155
			list( $user_token_key, $user_token_private ) = explode( '.', $user_token->secret );
156
			if ( $user_token_key === $user_token->secret ) {
157
				$user_token_key = '';
158
			}
159
		} else {
160
			$user_token_key = '';
161
		}
162
163
		return array(
164
			'id'         => $user->ID,
165
			'login'      => $user->user_login,
166
			'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
167
			'roles'      => $user->roles,
168
			'caps'       => $user->caps,
169
			'allcaps'    => $user->allcaps,
170
			'token_key'  => $user_token_key,
171
		);
172
	}
173
174
	function remote_authorize( $request ) {
175
		$user = get_user_by( 'id', $request['state'] );
176
		$this->tracking->record_user_event( 'jpc_remote_authorize_begin', array(), $user );
177
178
		foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
179
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
180
				return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), 'jpc_remote_authorize_fail' );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'missing_parameter'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
181
			}
182
		}
183
184
		if ( ! $user ) {
185
			return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ), 'jpc_remote_authorize_fail' );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'user_unknown'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
186
		}
187
188
		if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
189
			return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ), 'jpc_remote_authorize_fail' );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'already_connected'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
190
		}
191
192
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
193
194
		if ( is_a( $verified, 'IXR_Error' ) ) {
195
			return $this->error( $verified, 'jpc_remote_authorize_fail' );
196
		}
197
198
		wp_set_current_user( $request['state'] );
199
200
		$client_server = new Jetpack_Client_Server;
201
		$result = $client_server->authorize( $request );
202
203
		if ( is_wp_error( $result ) ) {
204
			return $this->error( $result, 'jpc_remote_authorize_fail' );
205
		}
206
207
		$this->tracking->record_user_event( 'jpc_remote_authorize_success' );
208
209
		return array(
210
			'result' => $result,
211
		);
212
	}
213
214
	/**
215
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
216
	 * register this site so that a plan can be provisioned.
217
	 *
218
	 * @param array $request An array containing at minimum nonce and local_user keys.
219
	 *
220
	 * @return WP_Error|array
221
	 */
222
	public function remote_register( $request ) {
223
		$this->tracking->record_user_event( 'jpc_remote_register_begin', array() );
224
225
		$user = $this->fetch_and_verify_local_user( $request );
226
227
		if ( ! $user ) {
228
			return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_register_fail' );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'input_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
229
		}
230
231
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
232
			return $this->error( $user, 'jpc_remote_register_fail' );
233
		}
234
235
		if ( empty( $request['nonce'] ) ) {
236
			return $this->error(
237
				new Jetpack_Error(
238
					'nonce_missing',
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'nonce_missing'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
239
					__( 'The required "nonce" parameter is missing.', 'jetpack' ),
240
					400
241
				),
242
				'jpc_remote_register_fail'
243
			);
244
		}
245
246
		$nonce = sanitize_text_field( $request['nonce'] );
247
		unset( $request['nonce'] );
248
249
		$api_url  = Jetpack::fix_url_for_bad_hosts( Jetpack::api_url( 'partner_provision_nonce_check' ) );
250
		$response = Client::_wp_remote_request(
251
			esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
252
			array( 'method' => 'GET' ),
253
			true
254
		);
255
256
		if (
257
			200 !== wp_remote_retrieve_response_code( $response ) ||
258
			'OK' !== trim( wp_remote_retrieve_body( $response ) )
259
		) {
260
			return $this->error(
261
				new Jetpack_Error(
262
					'invalid_nonce',
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'invalid_nonce'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
263
					__( 'There was an issue validating this request.', 'jetpack' ),
264
					400
265
				),
266
				'jpc_remote_register_fail'
267
			);
268
		}
269
270
		if ( ! Jetpack_Options::get_option( 'id' ) || ! Jetpack_Data::get_access_token() || ! empty( $request['force'] ) ) {
271
			wp_set_current_user( $user->ID );
272
273
			// This code mostly copied from Jetpack::admin_page_load.
274
			Jetpack::maybe_set_version_option();
275
			$registered = Jetpack::try_registration();
276
			if ( is_wp_error( $registered ) ) {
277
				return $this->error( $registered, 'jpc_remote_register_fail' );
278
			} elseif ( ! $registered ) {
279
				return $this->error(
280
					new Jetpack_Error(
281
						'registration_error',
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'registration_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
282
						__( 'There was an unspecified error registering the site', 'jetpack' ),
283
						400
284
					),
285
					'jpc_remote_register_fail'
286
				);
287
			}
288
		}
289
290
		$this->tracking->record_user_event( 'jpc_remote_register_success' );
291
292
		return array(
293
			'client_id' => Jetpack_Options::get_option( 'id' )
294
		);
295
	}
296
297
	/**
298
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
299
	 * register this site so that a plan can be provisioned.
300
	 *
301
	 * @param array $request An array containing at minimum a nonce key and a local_username key.
302
	 *
303
	 * @return WP_Error|array
304
	 */
305
	public function remote_provision( $request ) {
306
		$user = $this->fetch_and_verify_local_user( $request );
307
308
		if ( ! $user ) {
309
			return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_provision_fail' );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'input_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
310
		}
311
312
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
313
			return $this->error( $user, 'jpc_remote_provision_fail' );
314
		}
315
316
		$site_icon = get_site_icon_url();
317
318
		$auto_enable_sso = ( ! Jetpack::is_active() || Jetpack::is_module_active( 'sso' ) );
319
320
		/** This filter is documented in class.jetpack-cli.php */
321 View Code Duplication
		if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) {
322
			$redirect_uri = add_query_arg(
323
				array(
324
					'action'      => 'jetpack-sso',
325
					'redirect_to' => rawurlencode( admin_url() ),
326
				),
327
				wp_login_url() // TODO: come back to Jetpack dashboard?
328
			);
329
		} else {
330
			$redirect_uri = admin_url();
331
		}
332
333
		// Generate secrets.
334
		$role    = Jetpack::translate_user_to_role( $user );
335
		$secrets = Jetpack::init()->generate_secrets( 'authorize', $user->ID );
336
337
		$response = array(
338
			'jp_version'   => JETPACK__VERSION,
339
			'redirect_uri' => $redirect_uri,
340
			'user_id'      => $user->ID,
341
			'user_email'   => $user->user_email,
342
			'user_login'   => $user->user_login,
343
			'scope'        => Jetpack::sign_role( $role, $user->ID ),
344
			'secret'       => $secrets['secret_1'],
345
			'is_active'    => Jetpack::is_active(),
346
		);
347
348
		if ( $site_icon ) {
349
			$response['site_icon'] = $site_icon;
350
		}
351
352
		if ( ! empty( $request['onboarding'] ) ) {
353
			Jetpack::create_onboarding_token();
354
			$response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' );
355
		}
356
357
		return $response;
358
	}
359
360
	/**
361
	 * Given an array containing a local user identifier and a nonce, will attempt to fetch and set
362
	 * an access token for the given user.
363
	 *
364
	 * @param array $request An array containing local_user and nonce keys at minimum.
365
	 * @return mixed
366
	 */
367
	public function remote_connect( $request, $ixr_client = false ) {
368
		if ( Jetpack::is_active() ) {
369
			return $this->error(
370
				new WP_Error(
371
					'already_connected',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'already_connected'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
372
					__( 'Jetpack is already connected.', 'jetpack' ),
373
					400
374
				),
375
				'jpc_remote_connect_fail'
376
			);
377
		}
378
379
		$user = $this->fetch_and_verify_local_user( $request );
380
381
		if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
382
			return $this->error(
383
				new WP_Error(
384
					'input_error',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'input_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
385
					__( 'Valid user is required.', 'jetpack' ),
386
					400
387
				),
388
				'jpc_remote_connect_fail'
389
			);
390
		}
391
392
		if ( empty( $request['nonce'] ) ) {
393
			return $this->error(
394
				new WP_Error(
395
					'input_error',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'input_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
396
					__( 'A non-empty nonce must be supplied.', 'jetpack' ),
397
					400
398
				),
399
				'jpc_remote_connect_fail'
400
			);
401
		}
402
403
		if ( ! $ixr_client ) {
404
			Jetpack::load_xml_rpc_client();
405
			$ixr_client = new Jetpack_IXR_Client();
406
		}
407
		$ixr_client->query( 'jetpack.getUserAccessToken', array(
0 ignored issues
show
It seems like $ixr_client is not always an object, but can also be of type boolean. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
408
			'nonce'            => sanitize_text_field( $request['nonce'] ),
409
			'external_user_id' => $user->ID,
410
		) );
411
412
		$token = $ixr_client->isError() ? false : $ixr_client->getResponse();
413
		if ( empty( $token ) ) {
414
			return $this->error(
415
				new WP_Error(
416
					'token_fetch_failed',
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'token_fetch_failed'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
417
					__( 'Failed to fetch user token from WordPress.com.', 'jetpack' ),
418
					400
419
				),
420
				'jpc_remote_connect_fail'
421
			);
422
		}
423
		$token = sanitize_text_field( $token );
424
425
		Jetpack::update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );
426
427
		$this->do_post_authorization();
428
429
		return Jetpack::is_active();
430
	}
431
432
	private function fetch_and_verify_local_user( $request ) {
433
		if ( empty( $request['local_user'] ) ) {
434
			return $this->error(
435
				new Jetpack_Error(
436
					'local_user_missing',
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'local_user_missing'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
437
					__( 'The required "local_user" parameter is missing.', 'jetpack' ),
438
					400
439
				),
440
				'jpc_remote_provision_fail'
441
			);
442
		}
443
444
		// local user is used to look up by login, email or ID
445
		$local_user_info = $request['local_user'];
446
447
		return $this->get_user_by_anything( $local_user_info );
448
	}
449
450
	private function get_user_by_anything( $user_id ) {
451
		$user = get_user_by( 'login', $user_id );
452
453
		if ( ! $user ) {
454
			$user = get_user_by( 'email', $user_id );
455
		}
456
457
		if ( ! $user ) {
458
			$user = get_user_by( 'ID', $user_id );
459
		}
460
461
		return $user;
462
	}
463
464
	private function tracks_record_error( $name, $error, $user = null ) {
465
		if ( is_wp_error( $error ) ) {
466
			$this->tracking->record_user_event( $name, array(
467
				'error_code' => $error->get_error_code(),
468
				'error_message' => $error->get_error_message()
469
			), $user );
470
		} elseif( is_a( $error, 'IXR_Error' ) ) {
471
			$this->tracking->record_user_event( $name, array(
472
				'error_code' => $error->code,
473
				'error_message' => $error->message
474
			), $user );
475
		}
476
477
		return $error;
478
	}
479
480
	/**
481
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
482
	 *
483
	 * Possible error_codes:
484
	 *
485
	 * verify_secret_1_missing
486
	 * verify_secret_1_malformed
487
	 * verify_secrets_missing: verification secrets are not found in database
488
	 * verify_secrets_incomplete: verification secrets are only partially found in database
489
	 * verify_secrets_expired: verification secrets have expired
490
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
491
	 * state_missing: required parameter of state not found
492
	 * state_malformed: state is not a digit
493
	 * invalid_state: state in request does not match the stored state
494
	 *
495
	 * The 'authorize' and 'register' actions have additional error codes
496
	 *
497
	 * Possible values for action are `authorize`, `publicize` and `register`.
498
	 *
499
	 * state_missing: a state ( user id ) was not supplied
500
	 * state_malformed: state is not the correct data type
501
	 * invalid_state: supplied state does not match the stored state
502
	 */
503
	function verify_action( $params ) {
504
		$action = $params[0];
505
		$verify_secret = $params[1];
506
		$state = isset( $params[2] ) ? $params[2] : '';
507
		$user = get_user_by( 'id', $state );
508
		$tracks_failure_event_name = '';
509
510
		if ( 'authorize' === $action ) {
511
			$tracks_failure_event_name = 'jpc_verify_authorize_fail';
512
			$this->tracking->record_user_event( 'jpc_verify_authorize_begin', array(), $user );
513
		}
514
		if ( 'publicize' === $action ) {
515
			// This action is used on a response from a direct XML-RPC done from WordPress.com
516
			$tracks_failure_event_name = 'jpc_verify_publicize_fail';
517
			$this->tracking->record_user_event( 'jpc_verify_publicize_begin', array(), $user );
518
		}
519
		if ( 'register' === $action ) {
520
			$tracks_failure_event_name = 'jpc_verify_register_fail';
521
			$this->tracking->record_user_event( 'jpc_verify_register_begin', array(), $user );
522
		}
523
524
		if ( empty( $verify_secret ) ) {
525
			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 );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'verify_secret_1_missing'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
526
		} else if ( ! is_string( $verify_secret ) ) {
527
			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 );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'verify_secret_1_malformed'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
528
		} else if ( empty( $state ) ) {
529
			return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'state_missing'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
530
		} else if ( ! ctype_digit( $state ) ) {
531
			return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'state_malformed'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
532
		}
533
534
		$secrets = Jetpack::get_secrets( $action, $state );
535
536
		if ( ! $secrets ) {
537
			Jetpack::delete_secrets( $action, $state );
538
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification secrets not found', 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'verify_secrets_missing'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
539
		}
540
541
		if ( is_wp_error( $secrets ) ) {
542
			Jetpack::delete_secrets( $action, $state );
543
			return $this->error( new Jetpack_Error( $secrets->get_error_code(), $secrets->get_error_message(), 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The method get_error_code() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The call to Jetpack_Error::__construct() has too many arguments starting with $secrets->get_error_code().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
544
		}
545
546
		if ( empty( $secrets['secret_1'] ) || empty( $secrets['secret_2'] ) || empty( $secrets['exp'] ) ) {
547
			Jetpack::delete_secrets( $action, $state );
548
			return $this->error( new Jetpack_Error( 'verify_secrets_incomplete', 'Verification secrets are incomplete', 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'verify_secrets_incomplete'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
549
		}
550
551
		if ( ! hash_equals( $verify_secret, $secrets['secret_1'] ) ) {
552
			Jetpack::delete_secrets( $action, $state );
553
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ), $tracks_failure_event_name, $user );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'verify_secrets_mismatch'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
554
		}
555
556
		Jetpack::delete_secrets( $action, $state );
557
558
		if ( 'authorize' === $action ) {
559
			$this->tracking->record_user_event( 'jpc_verify_authorize_success', array(), $user );
560
		}
561
		if ( 'publicize' === $action ) {
562
			$this->tracking->record_user_event( 'jpc_verify_publicize_success', array(), $user );
563
		}
564
		if ( 'register' === $action ) {
565
			$this->tracking->record_user_event( 'jpc_verify_register_success', array(), $user );
566
		}
567
568
		return $secrets['secret_2'];
569
	}
570
571
	/**
572
	 * Wrapper for wp_authenticate( $username, $password );
573
	 *
574
	 * @return WP_User|bool
575
	 */
576
	function login() {
577
		Jetpack::init()->require_jetpack_authentication();
578
		$user = wp_authenticate( 'username', 'password' );
579
		if ( is_wp_error( $user ) ) {
580
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
581
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'invalid_request'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
582
			} else {
583
				$this->error = $user;
584
			}
585
			return false;
586
		} else if ( !$user ) { // Shouldn't happen.
587
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
0 ignored issues
show
The call to Jetpack_Error::__construct() has too many arguments starting with 'invalid_request'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
588
			return false;
589
		}
590
591
		return $user;
592
	}
593
594
	/**
595
	 * Returns the current error as an IXR_Error
596
	 *
597
	 * @return bool|IXR_Error
598
	 */
599
	function error( $error = null, $tracks_event_name = null, $user = null ) {
600
		// record using Tracks
601
		if ( null !== $tracks_event_name ) {
602
			$this->tracks_record_error( $tracks_event_name, $error, $user );
603
		}
604
605
		if ( !is_null( $error ) ) {
606
			$this->error = $error;
607
		}
608
609
		if ( is_wp_error( $this->error ) ) {
610
			$code = $this->error->get_error_data();
611
			if ( !$code ) {
612
				$code = -10520;
613
			}
614
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
615
			return new IXR_Error( $code, $message );
616
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
617
			return $this->error;
618
		}
619
620
		return false;
621
	}
622
623
/* API Methods */
624
625
	/**
626
	 * Just authenticates with the given Jetpack credentials.
627
	 *
628
	 * @return string The current Jetpack version number
629
	 */
630
	function test_connection() {
631
		return JETPACK__VERSION;
632
	}
633
634
	function test_api_user_code( $args ) {
635
		$client_id = (int) $args[0];
636
		$user_id   = (int) $args[1];
637
		$nonce     = (string) $args[2];
638
		$verify    = (string) $args[3];
639
640
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
641
			return false;
642
		}
643
644
		$user = get_user_by( 'id', $user_id );
645
		if ( !$user || is_wp_error( $user ) ) {
646
			return false;
647
		}
648
649
		/* debugging
650
		error_log( "CLIENT: $client_id" );
651
		error_log( "USER:   $user_id" );
652
		error_log( "NONCE:  $nonce" );
653
		error_log( "VERIFY: $verify" );
654
		*/
655
656
		$jetpack_token = Jetpack_Data::get_access_token( $user_id );
0 ignored issues
show
$user_id is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
657
658
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
659
		if ( !$api_user_code ) {
660
			return false;
661
		}
662
663
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
664
			'client_id' => (int) $client_id,
665
			'user_id'   => (int) $user_id,
666
			'nonce'     => (string) $nonce,
667
			'code'      => (string) $api_user_code,
668
		) ), $jetpack_token->secret );
669
670
		if ( ! hash_equals( $hmac, $verify ) ) {
671
			return false;
672
		}
673
674
		return $user_id;
675
	}
676
677
	/**
678
	* Disconnect this blog from the connected wordpress.com account
679
	* @return boolean
680
	*/
681
	function disconnect_blog() {
682
683
		// For tracking
684
		if ( ! empty( $this->user->ID ) ) {
685
			wp_set_current_user( $this->user->ID );
686
		}
687
688
		Jetpack::log( 'disconnect' );
689
		Jetpack::disconnect();
690
691
		return true;
692
	}
693
694
	/**
695
	 * Unlink a user from WordPress.com
696
	 *
697
	 * This will fail if called by the Master User.
698
	 */
699
	function unlink_user() {
700
		Jetpack::log( 'unlink' );
701
		return Jetpack::unlink_user();
702
	}
703
704
	/**
705
	 * Returns any object that is able to be synced
706
	 */
707
	function sync_object( $args ) {
708
		// e.g. posts, post, 5
709
		list( $module_name, $object_type, $id ) = $args;
710
711
		$sync_module = Jetpack_Sync_Modules::get_module( $module_name );
712
		$codec = Sender::get_instance()->get_codec();
713
714
		return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
715
	}
716
717
	/**
718
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
719
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
720
	 * and the remote Jetpack site.
721
	 *
722
	 * @return array
723
	 */
724
	function validate_urls_for_idc_mitigation() {
725
		return array(
726
			'home'    => Jetpack_Sync_Functions::home_url(),
727
			'siteurl' => Jetpack_Sync_Functions::site_url(),
728
		);
729
	}
730
731
	/**
732
	 * Returns what features are available. Uses the slug of the module files.
733
	 *
734
	 * @return array
735
	 */
736 View Code Duplication
	function features_available() {
737
		$raw_modules = Jetpack::get_available_modules();
738
		$modules = array();
739
		foreach ( $raw_modules as $module ) {
740
			$modules[] = Jetpack::get_module_slug( $module );
741
		}
742
743
		return $modules;
744
	}
745
746
	/**
747
	 * Returns what features are enabled. Uses the slug of the modules files.
748
	 *
749
	 * @return array
750
	 */
751 View Code Duplication
	function features_enabled() {
752
		$raw_modules = Jetpack::get_active_modules();
753
		$modules = array();
754
		foreach ( $raw_modules as $module ) {
755
			$modules[] = Jetpack::get_module_slug( $module );
756
		}
757
758
		return $modules;
759
	}
760
761
	function update_attachment_parent( $args ) {
762
		$attachment_id = (int) $args[0];
763
		$parent_id     = (int) $args[1];
764
765
		return wp_update_post( array(
766
			'ID'          => $attachment_id,
767
			'post_parent' => $parent_id,
768
		) );
769
	}
770
771
	function json_api( $args = array() ) {
772
		$json_api_args = $args[0];
773
		$verify_api_user_args = $args[1];
774
775
		$method       = (string) $json_api_args[0];
776
		$url          = (string) $json_api_args[1];
777
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
778
		$user_details = (array) $json_api_args[4];
779
		$locale       = (string) $json_api_args[5];
780
781
		if ( !$verify_api_user_args ) {
782
			$user_id = 0;
783
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
784
			$user_id = (int) $verify_api_user_args[1];
785
			if ( $user_id ) {
786
				$user = get_user_by( 'id', $user_id );
787
				if ( !$user || is_wp_error( $user ) ) {
788
					return false;
789
				}
790
			}
791
		} else {
792
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
793
			if ( !$user_id ) {
794
				return false;
795
			}
796
		}
797
798
		/* debugging
799
		error_log( "-- begin json api via jetpack debugging -- " );
800
		error_log( "METHOD: $method" );
801
		error_log( "URL: $url" );
802
		error_log( "POST BODY: $post_body" );
803
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
804
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
805
		error_log( "-- end json api via jetpack debugging -- " );
806
		*/
807
808
		if ( 'en' !== $locale ) {
809
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
810
			$new_locale = $locale;
811
			if ( strpos( $locale, '-' ) !== false ) {
812
				$locale_pieces = explode( '-', $locale );
813
				$new_locale = $locale_pieces[0];
814
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
815
			} else {
816
				// .com might pass 'fr' because thats what our language files are named as, where core seems
817
				// to do fr_FR - so try that if we don't think we can load the file.
818
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
819
					$new_locale =  $locale . '_' . strtoupper( $locale );
820
				}
821
			}
822
823
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
824
				unload_textdomain( 'default' );
825
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
826
			}
827
		}
828
829
		$old_user = wp_get_current_user();
830
		wp_set_current_user( $user_id );
831
832
		if ( $user_id ) {
833
			$token_key = false;
834
		} else {
835
			$jetpack   = Jetpack::init();
836
			$verified  = $jetpack->verify_xml_rpc_signature();
837
			$token_key = $verified['token_key'];
838
		}
839
840
		$token = Jetpack_Data::get_access_token( $user_id, $token_key );
841
		if ( !$token || is_wp_error( $token ) ) {
842
			return false;
843
		}
844
845
		define( 'REST_API_REQUEST', true );
846
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
847
848
		// needed?
849
		require_once ABSPATH . 'wp-admin/includes/admin.php';
850
851
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
852
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
853
		$api->token_details['user'] = $user_details;
854
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
855
856
		$display_errors = ini_set( 'display_errors', 0 );
857
		ob_start();
858
		$content_type = $api->serve( false );
859
		$output = ob_get_clean();
860
		ini_set( 'display_errors', $display_errors );
861
862
		$nonce = wp_generate_password( 10, false );
863
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
864
865
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
866
867
		return array(
868
			(string) $output,
869
			(string) $nonce,
870
			(string) $hmac,
871
		);
872
	}
873
874
	/**
875
	 * Handles authorization actions after connecting a site, such as enabling modules.
876
	 *
877
	 * This do_post_authorization() is used in this class, as opposed to calling
878
	 * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary.
879
	 *
880
	 * @return void
881
	 */
882
	public function do_post_authorization() {
883
		/** This filter is documented in class.jetpack-cli.php */
884
		$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
885
		Jetpack::handle_post_authorization_actions( $enable_sso, false, false );
886
	}
887
}
888