Completed
Push — add/mailchimp-groups-merge-fie... ( c88508...48f203 )
by
unknown
07:48 queued 01:05
created

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