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