Completed
Push — add/rna-connection-in-place ( d17300...c4ab4e )
by
unknown
151:44 queued 141:08
created

Jetpack_XMLRPC_Server::disconnect_blog()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack XMLRPC Server.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
use Automattic\Jetpack\Connection\Client;
9
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
10
use Automattic\Jetpack\Connection\Secrets;
11
use Automattic\Jetpack\Connection\Tokens;
12
use Automattic\Jetpack\Roles;
13
use Automattic\Jetpack\Sync\Functions;
14
use Automattic\Jetpack\Sync\Sender;
15
16
/**
17
 * Just a sack of functions.  Not actually an IXR_Server
18
 */
19
class Jetpack_XMLRPC_Server {
20
	/**
21
	 * The current error object
22
	 *
23
	 * @var \WP_Error
24
	 */
25
	public $error = null;
26
27
	/**
28
	 * The current user
29
	 *
30
	 * @var \WP_User
31
	 */
32
	public $user = null;
33
34
	/**
35
	 * The connection manager object.
36
	 *
37
	 * @var Automattic\Jetpack\Connection\Manager
38
	 */
39
	private $connection;
40
41
	/**
42
	 * Creates a new XMLRPC server object.
43
	 */
44
	public function __construct() {
45
		$this->connection = new Connection_Manager();
46
	}
47
48
	/**
49
	 * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
50
	 * user is not authenticated (->login()) then the methods are never added,
51
	 * so they will get a "does not exist" error.
52
	 *
53
	 * @param array $core_methods Core XMLRPC methods.
54
	 */
55
	public function xmlrpc_methods( $core_methods ) {
56
		$jetpack_methods = array(
57
			'jetpack.verifyAction'     => array( $this, 'verify_action' ),
58
			'jetpack.getUser'          => array( $this, 'get_user' ),
59
			'jetpack.remoteRegister'   => array( $this, 'remote_register' ),
60
			'jetpack.remoteProvision'  => array( $this, 'remote_provision' ),
61
			'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ),
62
			'jetpack.unlinkUser'       => array( $this, 'unlink_user' ),
63
		);
64
65
		if ( class_exists( 'Jetpack' ) ) {
66
			$jetpack_methods['jetpack.jsonAPI']           = array( $this, 'json_api' );
67
			$jetpack_methods['jetpack.testConnection']    = array( $this, 'test_connection' );
68
			$jetpack_methods['jetpack.featuresAvailable'] = array( $this, 'features_available' );
69
			$jetpack_methods['jetpack.featuresEnabled']   = array( $this, 'features_enabled' );
70
			$jetpack_methods['jetpack.disconnectBlog']    = array( $this, 'disconnect_blog' );
71
		}
72
73
		$this->user = $this->login();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->login() can also be of type boolean. However, the property $user is declared as type object<WP_User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
74
75
		if ( $this->user ) {
76
			$jetpack_methods = array_merge(
77
				$jetpack_methods,
78
				array(
79
					'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ),
80
				)
81
			);
82
83
			if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
84
				$jetpack_methods['metaWeblog.newMediaObject']      = $core_methods['metaWeblog.newMediaObject'];
85
				$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
86
			}
87
88
			/**
89
			 * Filters the XML-RPC methods available to Jetpack for authenticated users.
90
			 *
91
			 * @since 1.1.0
92
			 *
93
			 * @param array    $jetpack_methods XML-RPC methods available to the Jetpack Server.
94
			 * @param array    $core_methods    Available core XML-RPC methods.
95
			 * @param \WP_User $user            Information about a given WordPress user.
96
			 */
97
			$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $core_methods.

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...
98
		}
99
100
		/**
101
		 * Filters the XML-RPC methods available to Jetpack for requests signed only with a blog token.
102
		 *
103
		 * @since 3.0.0
104
		 *
105
		 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
106
		 * @param array $core_methods    Available core XML-RPC methods.
107
		 */
108
		return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $core_methods.

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...
109
	}
110
111
	/**
112
	 * Whitelist of the bootstrap XML-RPC methods
113
	 */
114
	public function bootstrap_xmlrpc_methods() {
115
		return array(
116
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
117
			'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
118
		);
119
	}
120
121
	/**
122
	 * Additional method needed for authorization calls.
123
	 */
124
	public function authorize_xmlrpc_methods() {
125
		return array(
126
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
127
		);
128
	}
129
130
	/**
131
	 * Remote provisioning methods.
132
	 */
133
	public function provision_xmlrpc_methods() {
134
		return array(
135
			'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
136
			'jetpack.remoteProvision' => array( $this, 'remote_provision' ),
137
			'jetpack.remoteConnect'   => array( $this, 'remote_connect' ),
138
			'jetpack.getUser'         => array( $this, 'get_user' ),
139
		);
140
	}
141
142
	/**
143
	 * Used to verify whether a local user exists and what role they have.
144
	 *
145
	 * @param int|string|array $request One of:
146
	 *                         int|string The local User's ID, username, or email address.
147
	 *                         array      A request array containing:
148
	 *                                    0: int|string The local User's ID, username, or email address.
149
	 *
150
	 * @return array|\IXR_Error Information about the user, or error if no such user found:
151
	 *                          roles:     string[] The user's rols.
152
	 *                          login:     string   The user's username.
153
	 *                          email_hash string[] The MD5 hash of the user's normalized email address.
154
	 *                          caps       string[] The user's capabilities.
155
	 *                          allcaps    string[] The user's granular capabilities, merged from role capabilities.
156
	 *                          token_key  string   The Token Key of the user's Jetpack token. Empty string if none.
157
	 */
158
	public function get_user( $request ) {
159
		$user_id = is_array( $request ) ? $request[0] : $request;
160
161
		if ( ! $user_id ) {
162
			return $this->error(
163
				new \WP_Error(
164
					'invalid_user',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
165
					__( 'Invalid user identifier.', 'jetpack' ),
166
					400
167
				),
168
				'get_user'
169
			);
170
		}
171
172
		$user = $this->get_user_by_anything( $user_id );
173
174
		if ( ! $user ) {
175
			return $this->error(
176
				new \WP_Error(
177
					'user_unknown',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
178
					__( 'User not found.', 'jetpack' ),
179
					404
180
				),
181
				'get_user'
182
			);
183
		}
184
185
		$user_token = ( new Tokens() )->get_access_token( $user->ID );
186
187
		if ( $user_token ) {
188
			list( $user_token_key ) = explode( '.', $user_token->secret );
189
			if ( $user_token_key === $user_token->secret ) {
190
				$user_token_key = '';
191
			}
192
		} else {
193
			$user_token_key = '';
194
		}
195
196
		return array(
197
			'id'         => $user->ID,
198
			'login'      => $user->user_login,
199
			'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
200
			'roles'      => $user->roles,
201
			'caps'       => $user->caps,
202
			'allcaps'    => $user->allcaps,
203
			'token_key'  => $user_token_key,
204
		);
205
	}
206
207
	/**
208
	 * Remote authorization XMLRPC method handler.
209
	 *
210
	 * @param array $request the request.
211
	 */
212
	public function remote_authorize( $request ) {
213
		$user = get_user_by( 'id', $request['state'] );
214
215
		/**
216
		 * Happens on various request handling events in the Jetpack XMLRPC server.
217
		 * The action combines several types of events:
218
		 *    - remote_authorize
219
		 *    - remote_provision
220
		 *    - get_user.
221
		 *
222
		 * @since 8.0.0
223
		 *
224
		 * @param String  $action the action name, i.e., 'remote_authorize'.
225
		 * @param String  $stage  the execution stage, can be 'begin', 'success', 'error', etc.
226
		 * @param array   $parameters extra parameters from the event.
227
		 * @param WP_User $user the acting user.
228
		 */
229
		do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user );
230
231
		foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
232
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
233
				return $this->error(
234
					new \WP_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ),
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
235
					'remote_authorize'
236
				);
237
			}
238
		}
239
240
		if ( ! $user ) {
241
			return $this->error( new \WP_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' );
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
242
		}
243
244
		if ( $this->connection->has_connected_owner() && $this->connection->is_user_connected( $request['state'] ) ) {
245
			return $this->error( new \WP_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' );
0 ignored issues
show
Unused Code introduced by
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...
246
		}
247
248
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
249
250
		if ( is_a( $verified, 'IXR_Error' ) ) {
251
			return $this->error( $verified, 'remote_authorize' );
252
		}
253
254
		wp_set_current_user( $request['state'] );
255
256
		$result = $this->connection->authorize( $request );
257
258
		if ( is_wp_error( $result ) ) {
259
			return $this->error( $result, 'remote_authorize' );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->connection->authorize($request) on line 256 can also be of type string; however, Jetpack_XMLRPC_Server::error() does only seem to accept object<WP_Error>|object<IXR_Error>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
260
		}
261
262
		// This action is documented in class.jetpack-xmlrpc-server.php.
263
		do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' );
264
265
		return array(
266
			'result' => $result,
267
		);
268
	}
269
270
	/**
271
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
272
	 * register this site so that a plan can be provisioned.
273
	 *
274
	 * @param array $request An array containing at minimum nonce and local_user keys.
275
	 *
276
	 * @return \WP_Error|array
277
	 */
278
	public function remote_register( $request ) {
279
		// This action is documented in class.jetpack-xmlrpc-server.php.
280
		do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() );
281
282
		$user = $this->fetch_and_verify_local_user( $request );
283
284
		if ( ! $user ) {
285
			return $this->error(
286
				new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ),
0 ignored issues
show
Unused Code introduced by
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...
287
				'remote_register'
288
			);
289
		}
290
291
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
292
			return $this->error( $user, 'remote_register' );
293
		}
294
295
		if ( empty( $request['nonce'] ) ) {
296
			return $this->error(
297
				new \WP_Error(
298
					'nonce_missing',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
299
					__( 'The required "nonce" parameter is missing.', 'jetpack' ),
300
					400
301
				),
302
				'remote_register'
303
			);
304
		}
305
306
		$nonce = sanitize_text_field( $request['nonce'] );
307
		unset( $request['nonce'] );
308
309
		$api_url  = $this->connection->api_url( 'partner_provision_nonce_check' );
310
		$response = Client::_wp_remote_request(
311
			esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
312
			array( 'method' => 'GET' ),
313
			true
314
		);
315
316
		if (
317
			200 !== wp_remote_retrieve_response_code( $response ) ||
318
			'OK' !== trim( wp_remote_retrieve_body( $response ) )
319
		) {
320
			return $this->error(
321
				new \WP_Error(
322
					'invalid_nonce',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
323
					__( 'There was an issue validating this request.', 'jetpack' ),
324
					400
325
				),
326
				'remote_register'
327
			);
328
		}
329
330
		if ( ! Jetpack_Options::get_option( 'id' ) || ! ( new Tokens() )->get_access_token() || ! empty( $request['force'] ) ) {
331
			wp_set_current_user( $user->ID );
332
333
			// This code mostly copied from Jetpack::admin_page_load.
334
			Jetpack::maybe_set_version_option();
335
			$registered = Jetpack::try_registration();
336
			if ( is_wp_error( $registered ) ) {
337
				return $this->error( $registered, 'remote_register' );
338
			} elseif ( ! $registered ) {
339
				return $this->error(
340
					new \WP_Error(
341
						'registration_error',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
342
						__( 'There was an unspecified error registering the site', 'jetpack' ),
343
						400
344
					),
345
					'remote_register'
346
				);
347
			}
348
		}
349
350
		// This action is documented in class.jetpack-xmlrpc-server.php.
351
		do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' );
352
353
		return array(
354
			'client_id' => Jetpack_Options::get_option( 'id' ),
355
		);
356
	}
357
358
	/**
359
	 * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
360
	 * register this site so that a plan can be provisioned.
361
	 *
362
	 * @param array $request An array containing at minimum a nonce key and a local_username key.
363
	 *
364
	 * @return \WP_Error|array
365
	 */
366
	public function remote_provision( $request ) {
367
		$user = $this->fetch_and_verify_local_user( $request );
368
369
		if ( ! $user ) {
370
			return $this->error(
371
				new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ),
0 ignored issues
show
Unused Code introduced by
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...
372
				'remote_provision'
373
			);
374
		}
375
376
		if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
377
			return $this->error( $user, 'remote_provision' );
378
		}
379
380
		$site_icon = get_site_icon_url();
381
382
		$auto_enable_sso = ( ! $this->connection->has_connected_owner() || Jetpack::is_module_active( 'sso' ) );
383
384
		/** This filter is documented in class.jetpack-cli.php */
385 View Code Duplication
		if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) {
386
			$redirect_uri = add_query_arg(
387
				array(
388
					'action'      => 'jetpack-sso',
389
					'redirect_to' => rawurlencode( admin_url() ),
390
				),
391
				wp_login_url() // TODO: come back to Jetpack dashboard?
392
			);
393
		} else {
394
			$redirect_uri = admin_url();
395
		}
396
397
		// Generate secrets.
398
		$roles   = new Roles();
399
		$role    = $roles->translate_user_to_role( $user );
400
		$secrets = ( new Secrets() )->generate( 'authorize', $user->ID );
401
402
		$response = array(
403
			'jp_version'   => JETPACK__VERSION,
404
			'redirect_uri' => $redirect_uri,
405
			'user_id'      => $user->ID,
406
			'user_email'   => $user->user_email,
407
			'user_login'   => $user->user_login,
408
			'scope'        => $this->connection->sign_role( $role, $user->ID ),
409
			'secret'       => $secrets['secret_1'],
410
			'is_active'    => $this->connection->has_connected_owner(),
411
		);
412
413
		if ( $site_icon ) {
414
			$response['site_icon'] = $site_icon;
415
		}
416
417
		if ( ! empty( $request['onboarding'] ) ) {
418
			Jetpack::create_onboarding_token();
419
			$response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' );
420
		}
421
422
		return $response;
423
	}
424
425
	/**
426
	 * Given an array containing a local user identifier and a nonce, will attempt to fetch and set
427
	 * an access token for the given user.
428
	 *
429
	 * @param array       $request    An array containing local_user and nonce keys at minimum.
430
	 * @param \IXR_Client $ixr_client The client object, optional.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $ixr_client not be false|IXR_Client?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
431
	 * @return mixed
432
	 */
433
	public function remote_connect( $request, $ixr_client = false ) {
434
		if ( $this->connection->has_connected_owner() ) {
435
			return $this->error(
436
				new WP_Error(
437
					'already_connected',
0 ignored issues
show
Unused Code introduced by
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...
438
					__( 'Jetpack is already connected.', 'jetpack' ),
439
					400
440
				),
441
				'remote_connect'
442
			);
443
		}
444
445
		$user = $this->fetch_and_verify_local_user( $request );
446
447
		if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
448
			return $this->error(
449
				new WP_Error(
450
					'input_error',
0 ignored issues
show
Unused Code introduced by
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...
451
					__( 'Valid user is required.', 'jetpack' ),
452
					400
453
				),
454
				'remote_connect'
455
			);
456
		}
457
458
		if ( empty( $request['nonce'] ) ) {
459
			return $this->error(
460
				new WP_Error(
461
					'input_error',
0 ignored issues
show
Unused Code introduced by
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...
462
					__( 'A non-empty nonce must be supplied.', 'jetpack' ),
463
					400
464
				),
465
				'remote_connect'
466
			);
467
		}
468
469
		if ( ! $ixr_client ) {
470
			$ixr_client = new Jetpack_IXR_Client();
471
		}
472
		// TODO: move this query into the Tokens class?
473
		$ixr_client->query(
474
			'jetpack.getUserAccessToken',
475
			array(
476
				'nonce'            => sanitize_text_field( $request['nonce'] ),
477
				'external_user_id' => $user->ID,
478
			)
479
		);
480
481
		$token = $ixr_client->isError() ? false : $ixr_client->getResponse();
482
		if ( empty( $token ) ) {
483
			return $this->error(
484
				new WP_Error(
485
					'token_fetch_failed',
0 ignored issues
show
Unused Code introduced by
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...
486
					__( 'Failed to fetch user token from WordPress.com.', 'jetpack' ),
487
					400
488
				),
489
				'remote_connect'
490
			);
491
		}
492
		$token = sanitize_text_field( $token );
493
494
		( new Tokens() )->update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );
495
496
		$this->do_post_authorization();
497
498
		return $this->connection->has_connected_owner();
499
	}
500
501
	/**
502
	 * Getter for the local user to act as.
503
	 *
504
	 * @param array $request the current request data.
505
	 */
506
	private function fetch_and_verify_local_user( $request ) {
507
		if ( empty( $request['local_user'] ) ) {
508
			return $this->error(
509
				new \WP_Error(
510
					'local_user_missing',
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
511
					__( 'The required "local_user" parameter is missing.', 'jetpack' ),
512
					400
513
				),
514
				'remote_provision'
515
			);
516
		}
517
518
		// Local user is used to look up by login, email or ID.
519
		$local_user_info = $request['local_user'];
520
521
		return $this->get_user_by_anything( $local_user_info );
522
	}
523
524
	/**
525
	 * Gets the user object by its data.
526
	 *
527
	 * @param string $user_id can be any identifying user data.
528
	 */
529
	private function get_user_by_anything( $user_id ) {
530
		$user = get_user_by( 'login', $user_id );
531
532
		if ( ! $user ) {
533
			$user = get_user_by( 'email', $user_id );
534
		}
535
536
		if ( ! $user ) {
537
			$user = get_user_by( 'ID', $user_id );
538
		}
539
540
		return $user;
541
	}
542
543
	/**
544
	 * Possible error_codes:
545
	 *
546
	 * - verify_secret_1_missing
547
	 * - verify_secret_1_malformed
548
	 * - verify_secrets_missing: verification secrets are not found in database
549
	 * - verify_secrets_incomplete: verification secrets are only partially found in database
550
	 * - verify_secrets_expired: verification secrets have expired
551
	 * - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
552
	 * - state_missing: required parameter of state not found
553
	 * - state_malformed: state is not a digit
554
	 * - invalid_state: state in request does not match the stored state
555
	 *
556
	 * The 'authorize' and 'register' actions have additional error codes
557
	 *
558
	 * state_missing: a state ( user id ) was not supplied
559
	 * state_malformed: state is not the correct data type
560
	 * invalid_state: supplied state does not match the stored state
561
	 *
562
	 * @param array $params action An array of 3 parameters:
563
	 *     [0]: string action. Possible values are `authorize`, `publicize` and `register`.
564
	 *     [1]: string secret_1.
565
	 *     [2]: int state.
566
	 * @return \IXR_Error|string IXR_Error on failure, secret_2 on success.
567
	 */
568
	public function verify_action( $params ) {
569
		$action        = isset( $params[0] ) ? $params[0] : '';
570
		$verify_secret = isset( $params[1] ) ? $params[1] : '';
571
		$state         = isset( $params[2] ) ? $params[2] : '';
572
573
		$result = ( new Secrets() )->verify( $action, $verify_secret, $state );
574
575
		if ( is_wp_error( $result ) ) {
576
			return $this->error( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by (new \Automattic\Jetpack...$verify_secret, $state) on line 573 can also be of type string; however, Jetpack_XMLRPC_Server::error() does only seem to accept object<WP_Error>|object<IXR_Error>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
577
		}
578
579
		return $result;
580
	}
581
582
	/**
583
	 * Wrapper for wp_authenticate( $username, $password );
584
	 *
585
	 * @return \WP_User|bool
586
	 */
587
	public function login() {
588
		$this->connection->require_jetpack_authentication();
589
		$user = wp_authenticate( 'username', 'password' );
590
		if ( is_wp_error( $user ) ) {
591
			if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything.
592
				$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
593
			} else {
594
				$this->error = $user;
595
			}
596
			return false;
597
		} elseif ( ! $user ) { // Shouldn't happen.
598
			$this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_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...
599
			return false;
600
		}
601
602
		wp_set_current_user( $user->ID );
603
604
		return $user;
605
	}
606
607
	/**
608
	 * Returns the current error as an \IXR_Error
609
	 *
610
	 * @param \WP_Error|\IXR_Error $error             The error object, optional.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $error not be WP_Error|IXR_Error|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
611
	 * @param string               $event_name The event name.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $event_name not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
612
	 * @param \WP_User             $user              The user object.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $user not be WP_User|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
613
	 * @return bool|\IXR_Error
614
	 */
615
	public function error( $error = null, $event_name = null, $user = null ) {
616
		if ( null !== $event_name ) {
617
			// This action is documented in class.jetpack-xmlrpc-server.php.
618
			do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user );
619
		}
620
621
		if ( ! is_null( $error ) ) {
622
			$this->error = $error;
0 ignored issues
show
Documentation Bug introduced by
It seems like $error can also be of type object<IXR_Error>. However, the property $error is declared as type object<WP_Error>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
623
		}
624
625
		if ( is_wp_error( $this->error ) ) {
626
			$code = $this->error->get_error_data();
0 ignored issues
show
Bug introduced by
The method get_error_data() 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...
627
			if ( ! $code ) {
628
				$code = -10520;
629
			}
630
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
0 ignored issues
show
Bug introduced by
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...
Bug introduced by
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...
631
			return new \IXR_Error( $code, $message );
632
		} elseif ( is_a( $this->error, 'IXR_Error' ) ) {
633
			return $this->error;
634
		}
635
636
		return false;
637
	}
638
639
	/* API Methods */
640
641
	/**
642
	 * Just authenticates with the given Jetpack credentials.
643
	 *
644
	 * @return string The current Jetpack version number
645
	 */
646
	public function test_connection() {
647
		return JETPACK__VERSION;
648
	}
649
650
	/**
651
	 * Test the API user code.
652
	 *
653
	 * @param array $args arguments identifying the test site.
654
	 */
655
	public function test_api_user_code( $args ) {
656
		$client_id = (int) $args[0];
657
		$user_id   = (int) $args[1];
658
		$nonce     = (string) $args[2];
659
		$verify    = (string) $args[3];
660
661
		if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) {
662
			return false;
663
		}
664
665
		$user = get_user_by( 'id', $user_id );
666
		if ( ! $user || is_wp_error( $user ) ) {
667
			return false;
668
		}
669
670
		/* phpcs:ignore
671
		 debugging
672
		error_log( "CLIENT: $client_id" );
673
		error_log( "USER:   $user_id" );
674
		error_log( "NONCE:  $nonce" );
675
		error_log( "VERIFY: $verify" );
676
		*/
677
678
		$jetpack_token = ( new Tokens() )->get_access_token( $user_id );
679
680
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
681
		if ( ! $api_user_code ) {
682
			return false;
683
		}
684
685
		$hmac = hash_hmac(
686
			'md5',
687
			json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
688
				(object) array(
689
					'client_id' => (int) $client_id,
690
					'user_id'   => (int) $user_id,
691
					'nonce'     => (string) $nonce,
692
					'code'      => (string) $api_user_code,
693
				)
694
			),
695
			$jetpack_token->secret
696
		);
697
698
		if ( ! hash_equals( $hmac, $verify ) ) {
699
			return false;
700
		}
701
702
		return $user_id;
703
	}
704
705
	/**
706
	 * Disconnect this blog from the connected wordpress.com account
707
	 *
708
	 * @return boolean
709
	 */
710
	public function disconnect_blog() {
711
712
		// For tracking.
713
		if ( ! empty( $this->user->ID ) ) {
714
			wp_set_current_user( $this->user->ID );
715
		}
716
717
		/**
718
		 * Fired when we want to log an event to the Jetpack event log.
719
		 *
720
		 * @since 7.7.0
721
		 *
722
		 * @param string $code Unique name for the event.
723
		 * @param string $data Optional data about the event.
724
		 */
725
		do_action( 'jetpack_event_log', 'disconnect' );
726
		Jetpack::disconnect();
727
728
		return true;
729
	}
730
731
	/**
732
	 * Unlink a user from WordPress.com
733
	 *
734
	 * When the request is done without any parameter, this XMLRPC callback gets an empty array as input.
735
	 *
736
	 * If $user_id is not provided, it will try to disconnect the current logged in user. This will fail if called by the Master User.
737
	 *
738
	 * If $user_id is is provided, it will try to disconnect the informed user, even if it's the Master User.
739
	 *
740
	 * @param mixed $user_id The user ID to disconnect from this site.
741
	 */
742
	public function unlink_user( $user_id = array() ) {
743
		$user_id = (int) $user_id;
744
		if ( $user_id < 1 ) {
745
			$user_id = null;
746
		}
747
		/**
748
		 * Fired when we want to log an event to the Jetpack event log.
749
		 *
750
		 * @since 7.7.0
751
		 *
752
		 * @param string $code Unique name for the event.
753
		 * @param string $data Optional data about the event.
754
		 */
755
		do_action( 'jetpack_event_log', 'unlink' );
756
		return $this->connection->disconnect_user(
757
			$user_id,
758
			(bool) $user_id
759
		);
760
	}
761
762
	/**
763
	 * Returns any object that is able to be synced.
764
	 *
765
	 * @deprecated since 7.8.0
766
	 * @see Automattic\Jetpack\Sync\Sender::sync_object()
767
	 *
768
	 * @param array $args the synchronized object parameters.
769
	 * @return string Encoded sync object.
770
	 */
771
	public function sync_object( $args ) {
772
		_deprecated_function( __METHOD__, 'jetpack-7.8', 'Automattic\\Jetpack\\Sync\\Sender::sync_object' );
773
		return Sender::get_instance()->sync_object( $args );
774
	}
775
776
	/**
777
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
778
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
779
	 * and the remote Jetpack site.
780
	 *
781
	 * @return array
782
	 */
783
	public function validate_urls_for_idc_mitigation() {
784
		return array(
785
			'home'    => Functions::home_url(),
786
			'siteurl' => Functions::site_url(),
787
		);
788
	}
789
790
	/**
791
	 * Returns what features are available. Uses the slug of the module files.
792
	 *
793
	 * @return array
794
	 */
795 View Code Duplication
	public function features_available() {
796
		$raw_modules = Jetpack::get_available_modules();
797
		$modules     = array();
798
		foreach ( $raw_modules as $module ) {
799
			$modules[] = Jetpack::get_module_slug( $module );
800
		}
801
802
		return $modules;
803
	}
804
805
	/**
806
	 * Returns what features are enabled. Uses the slug of the modules files.
807
	 *
808
	 * @return array
809
	 */
810 View Code Duplication
	public function features_enabled() {
811
		$raw_modules = Jetpack::get_active_modules();
812
		$modules     = array();
813
		foreach ( $raw_modules as $module ) {
814
			$modules[] = Jetpack::get_module_slug( $module );
815
		}
816
817
		return $modules;
818
	}
819
820
	/**
821
	 * Updates the attachment parent object.
822
	 *
823
	 * @param array $args attachment and parent identifiers.
824
	 */
825
	public function update_attachment_parent( $args ) {
826
		$attachment_id = (int) $args[0];
827
		$parent_id     = (int) $args[1];
828
829
		return wp_update_post(
830
			array(
831
				'ID'          => $attachment_id,
832
				'post_parent' => $parent_id,
833
			)
834
		);
835
	}
836
837
	/**
838
	 * Serve a JSON API request.
839
	 *
840
	 * @param array $args request arguments.
841
	 */
842
	public function json_api( $args = array() ) {
843
		$json_api_args        = $args[0];
844
		$verify_api_user_args = $args[1];
845
846
		$method       = (string) $json_api_args[0];
847
		$url          = (string) $json_api_args[1];
848
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
849
		$user_details = (array) $json_api_args[4];
850
		$locale       = (string) $json_api_args[5];
851
852
		if ( ! $verify_api_user_args ) {
853
			$user_id = 0;
854
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
855
			$user_id = (int) $verify_api_user_args[1];
856
			if ( $user_id ) {
857
				$user = get_user_by( 'id', $user_id );
858
				if ( ! $user || is_wp_error( $user ) ) {
859
					return false;
860
				}
861
			}
862
		} else {
863
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
864
			if ( ! $user_id ) {
865
				return false;
866
			}
867
		}
868
869
		/* phpcs:ignore
870
		 debugging
871
		error_log( "-- begin json api via jetpack debugging -- " );
872
		error_log( "METHOD: $method" );
873
		error_log( "URL: $url" );
874
		error_log( "POST BODY: $post_body" );
875
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
876
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
877
		error_log( "-- end json api via jetpack debugging -- " );
878
		*/
879
880
		if ( 'en' !== $locale ) {
881
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
882
			$new_locale = $locale;
883
			if ( strpos( $locale, '-' ) !== false ) {
884
				$locale_pieces = explode( '-', $locale );
885
				$new_locale    = $locale_pieces[0];
886
				$new_locale   .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
887
			} else {
888
				// .com might pass 'fr' because thats what our language files are named as, where core seems
889
				// to do fr_FR - so try that if we don't think we can load the file.
890
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
891
					$new_locale = $locale . '_' . strtoupper( $locale );
892
				}
893
			}
894
895
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
896
				unload_textdomain( 'default' );
897
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
898
			}
899
		}
900
901
		$old_user = wp_get_current_user();
902
		wp_set_current_user( $user_id );
903
904
		if ( $user_id ) {
905
			$token_key = false;
906
		} else {
907
			$verified  = $this->connection->verify_xml_rpc_signature();
908
			$token_key = $verified['token_key'];
909
		}
910
911
		$token = ( new Tokens() )->get_access_token( $user_id, $token_key );
912
		if ( ! $token || is_wp_error( $token ) ) {
913
			return false;
914
		}
915
916
		define( 'REST_API_REQUEST', true );
917
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
918
919
		// needed?
920
		require_once ABSPATH . 'wp-admin/includes/admin.php';
921
922
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
923
		$api                        = WPCOM_JSON_API::init( $method, $url, $post_body );
924
		$api->token_details['user'] = $user_details;
925
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
926
927
		$display_errors = ini_set( 'display_errors', 0 ); // phpcs:ignore WordPress.PHP.IniSet
928
		ob_start();
929
		$api->serve( false );
930
		$output = ob_get_clean();
931
		ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet
932
933
		$nonce = wp_generate_password( 10, false );
934
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
935
936
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
937
938
		return array(
939
			(string) $output,
940
			(string) $nonce,
941
			(string) $hmac,
942
		);
943
	}
944
945
	/**
946
	 * Handles authorization actions after connecting a site, such as enabling modules.
947
	 *
948
	 * This do_post_authorization() is used in this class, as opposed to calling
949
	 * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary.
950
	 *
951
	 * @return void
952
	 */
953
	public function do_post_authorization() {
954
		/** This filter is documented in class.jetpack-cli.php */
955
		$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
956
		Jetpack::handle_post_authorization_actions( $enable_sso, false, false );
957
	}
958
}
959