Completed
Push — try/add-jetpack-purchase-token ( 0a4d98...ef8438 )
by
unknown
10:51
created

REST_Connector::connection_authorize_url()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 8
nop 1
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Sets up the Connection REST API endpoints.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
use Automattic\Jetpack\Status;
11
use Jetpack_XMLRPC_Server;
12
use WP_Error;
13
use WP_REST_Request;
14
use WP_REST_Response;
15
use WP_REST_Server;
16
17
/**
18
 * Registers the REST routes for Connections.
19
 */
20
class REST_Connector {
21
	/**
22
	 * The Connection Manager.
23
	 *
24
	 * @var Manager
25
	 */
26
	private $connection;
27
28
	/**
29
	 * This property stores the localized "Insufficient Permissions" error message.
30
	 *
31
	 * @var string Generic error message when user is not allowed to perform an action.
32
	 */
33
	private static $user_permissions_error_msg;
34
35
	const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
36
	. 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
37
	. '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
38
	. '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
39
	. 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
40
	. 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
41
	. 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
42
	. 'EwIDAQAB' . "\r\n"
43
	. '-----END PUBLIC KEY-----' . "\r\n";
44
45
	/**
46
	 * Constructor.
47
	 *
48
	 * @param Manager $connection The Connection Manager.
49
	 */
50
	public function __construct( Manager $connection ) {
51
		$this->connection = $connection;
52
53
		self::$user_permissions_error_msg = esc_html__(
54
			'You do not have the correct user permissions to perform this action.
55
			Please contact your site admin if you think this is a mistake.',
56
			'jetpack'
57
		);
58
59
		if ( ! $this->connection->has_connected_owner() ) {
60
			// Register a site.
61
			register_rest_route(
62
				'jetpack/v4',
63
				'/verify_registration',
64
				array(
65
					'methods'             => WP_REST_Server::EDITABLE,
66
					'callback'            => array( $this, 'verify_registration' ),
67
					'permission_callback' => '__return_true',
68
				)
69
			);
70
		}
71
72
		// Authorize a remote user.
73
		register_rest_route(
74
			'jetpack/v4',
75
			'/remote_authorize',
76
			array(
77
				'methods'             => WP_REST_Server::EDITABLE,
78
				'callback'            => __CLASS__ . '::remote_authorize',
79
				'permission_callback' => '__return_true',
80
			)
81
		);
82
83
		// Get current connection status of Jetpack.
84
		register_rest_route(
85
			'jetpack/v4',
86
			'/connection',
87
			array(
88
				'methods'             => WP_REST_Server::READABLE,
89
				'callback'            => __CLASS__ . '::connection_status',
90
				'permission_callback' => '__return_true',
91
			)
92
		);
93
94
		// Get list of plugins that use the Jetpack connection.
95
		register_rest_route(
96
			'jetpack/v4',
97
			'/connection/plugins',
98
			array(
99
				'methods'             => WP_REST_Server::READABLE,
100
				'callback'            => array( $this, 'get_connection_plugins' ),
101
				'permission_callback' => __CLASS__ . '::connection_plugins_permission_check',
102
			)
103
		);
104
105
		// Full or partial reconnect in case of connection issues.
106
		register_rest_route(
107
			'jetpack/v4',
108
			'/connection/reconnect',
109
			array(
110
				'methods'             => WP_REST_Server::EDITABLE,
111
				'callback'            => array( $this, 'connection_reconnect' ),
112
				'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check',
113
			)
114
		);
115
116
		// Register the site (get `blog_token`).
117
		register_rest_route(
118
			'jetpack/v4',
119
			'/connection/register',
120
			array(
121
				'methods'             => WP_REST_Server::EDITABLE,
122
				'callback'            => array( $this, 'connection_register' ),
123
				'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
124
				'args'                => array(
125
					'from'               => array(
126
						'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack' ),
127
						'type'        => 'string',
128
					),
129
					'registration_nonce' => array(
130
						'description' => __( 'The registration nonce', 'jetpack' ),
131
						'type'        => 'string',
132
						'required'    => true,
133
					),
134
					'no_iframe'          => array(
135
						'description' => __( 'Disable In-Place connection flow and go straight to Calypso', 'jetpack' ),
136
						'type'        => 'boolean',
137
					),
138
					'redirect_uri'       => array(
139
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack' ),
140
						'type'        => 'string',
141
					),
142
				),
143
			)
144
		);
145
146
		// Get authorization URL.
147
		register_rest_route(
148
			'jetpack/v4',
149
			'/connection/authorize_url',
150
			array(
151
				'methods'             => WP_REST_Server::READABLE,
152
				'callback'            => array( $this, 'connection_authorize_url' ),
153
				'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
154
				'args'                => array(
155
					'no_iframe'    => array(
156
						'description' => __( 'Disable In-Place connection flow and go straight to Calypso', 'jetpack' ),
157
						'type'        => 'boolean',
158
					),
159
					'redirect_uri' => array(
160
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack' ),
161
						'type'        => 'string',
162
					),
163
				),
164
			)
165
		);
166
	}
167
168
	/**
169
	 * Handles verification that a site is registered.
170
	 *
171
	 * @since 5.4.0
172
	 *
173
	 * @param WP_REST_Request $request The request sent to the WP REST API.
174
	 *
175
	 * @return string|WP_Error
176
	 */
177
	public function verify_registration( WP_REST_Request $request ) {
178
		$registration_data = array( $request['secret_1'], $request['state'] );
179
180
		return $this->connection->handle_registration( $registration_data );
181
	}
182
183
	/**
184
	 * Handles verification that a site is registered
185
	 *
186
	 * @since 5.4.0
187
	 *
188
	 * @param WP_REST_Request $request The request sent to the WP REST API.
189
	 *
190
	 * @return array|wp-error
0 ignored issues
show
Documentation introduced by
The doc-type array|wp-error could not be parsed: Unknown type name "wp-error" at position 6. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
191
	 */
192
	public static function remote_authorize( $request ) {
193
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
194
		$result        = $xmlrpc_server->remote_authorize( $request );
195
196
		if ( is_a( $result, 'IXR_Error' ) ) {
197
			$result = new WP_Error( $result->code, $result->message );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $result->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...
198
		}
199
200
		return $result;
201
	}
202
203
	/**
204
	 * Get connection status for this Jetpack site.
205
	 *
206
	 * @since 4.3.0
207
	 *
208
	 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
209
	 *
210
	 * @return WP_REST_Response|array Connection information.
211
	 */
212
	public static function connection_status( $rest_response = true ) {
213
		$status     = new Status();
214
		$connection = new Manager();
215
216
		$connection_status = array(
217
			'isActive'          => $connection->is_active(), // TODO deprecate this.
0 ignored issues
show
Deprecated Code introduced by
The method Automattic\Jetpack\Connection\Manager::is_active() has been deprecated with message: 9.6.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
218
			'isStaging'         => $status->is_staging_site(),
219
			'isRegistered'      => $connection->is_connected(),
220
			'isUserConnected'   => $connection->is_user_connected(),
221
			'hasConnectedOwner' => $connection->has_connected_owner(),
222
			'offlineMode'       => array(
223
				'isActive'        => $status->is_offline_mode(),
224
				'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
225
				'url'             => $status->is_local_site(),
226
				/** This filter is documented in packages/status/src/class-status.php */
227
				'filter'          => ( apply_filters( 'jetpack_development_mode', false ) || apply_filters( 'jetpack_offline_mode', false ) ), // jetpack_development_mode is deprecated.
228
				'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
229
			),
230
			'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
231
		);
232
233
		/**
234
		 * Filters the connection status data.
235
		 *
236
		 * @since 9.6.0
237
		 *
238
		 * @param array An array containing the connection status data.
239
		 */
240
		$connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
241
242
		if ( $rest_response ) {
243
			return rest_ensure_response(
244
				$connection_status
245
			);
246
		} else {
247
			return $connection_status;
248
		}
249
	}
250
251
	/**
252
	 * Get plugins connected to the Jetpack.
253
	 *
254
	 * @since 8.6.0
255
	 *
256
	 * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
257
	 */
258
	public function get_connection_plugins() {
259
		$plugins = $this->connection->get_connected_plugins();
260
261
		if ( is_wp_error( $plugins ) ) {
262
			return $plugins;
263
		}
264
265
		array_walk(
266
			$plugins,
267
			function ( &$data, $slug ) {
268
				$data['slug'] = $slug;
269
			}
270
		);
271
272
		return rest_ensure_response( array_values( $plugins ) );
273
	}
274
275
	/**
276
	 * Verify that user can view Jetpack admin page and can activate plugins.
277
	 *
278
	 * @since 8.8.0
279
	 *
280
	 * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
281
	 */
282
	public static function activate_plugins_permission_check() {
283
		if ( current_user_can( 'activate_plugins' ) ) {
284
			return true;
285
		}
286
287
		return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_user_permission_activate_plugins'.

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...
288
	}
289
290
	/**
291
	 * Permission check for the connection_plugins endpoint
292
	 *
293
	 * @return bool|WP_Error
294
	 */
295
	public static function connection_plugins_permission_check() {
296
		if ( true === static::activate_plugins_permission_check() ) {
297
			return true;
298
		}
299
300
		if ( true === static::is_request_signed_by_jetpack_debugger() ) {
301
			return true;
302
		}
303
304
		return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_user_permission_activate_plugins'.

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...
305
306
	}
307
308
	/**
309
	 * Verifies if the request was signed with the Jetpack Debugger key
310
	 *
311
	 * @param string|null $pub_key The public key used to verify the signature. Default is the Jetpack Debugger key. This is used for testing purposes.
312
	 *
313
	 * @return bool
314
	 */
315
	public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
316
		 // phpcs:disable WordPress.Security.NonceVerification.Recommended
317
		if ( ! isset( $_GET['signature'], $_GET['timestamp'], $_GET['url'], $_GET['rest_route'] ) ) {
318
			return false;
319
		}
320
321
		// signature timestamp must be within 5min of current time.
322
		if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
323
			return false;
324
		}
325
326
		$signature = base64_decode( $_GET['signature'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
327
328
		$signature_data = wp_json_encode(
329
			array(
330
				'rest_route' => $_GET['rest_route'],
331
				'timestamp'  => (int) $_GET['timestamp'],
332
				'url'        => wp_unslash( $_GET['url'] ),
333
			)
334
		);
335
336
		if (
337
			! function_exists( 'openssl_verify' )
338
			|| 1 !== openssl_verify(
339
				$signature_data,
340
				$signature,
341
				$pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
342
			)
343
		) {
344
			return false;
345
		}
346
347
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
348
349
		return true;
350
	}
351
352
	/**
353
	 * Verify that user is allowed to disconnect Jetpack.
354
	 *
355
	 * @since 8.8.0
356
	 *
357
	 * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
358
	 */
359
	public static function jetpack_reconnect_permission_check() {
360
		if ( current_user_can( 'jetpack_reconnect' ) ) {
361
			return true;
362
		}
363
364
		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_user_permission_jetpack_disconnect'.

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...
365
	}
366
367
	/**
368
	 * Returns generic error message when user is not allowed to perform an action.
369
	 *
370
	 * @return string The error message.
371
	 */
372
	public static function get_user_permissions_error_msg() {
373
		return self::$user_permissions_error_msg;
374
	}
375
376
	/**
377
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
378
	 *
379
	 * @since 8.8.0
380
	 *
381
	 * @return \WP_REST_Response|WP_Error
382
	 */
383
	public function connection_reconnect() {
384
		$response = array();
385
386
		$next = null;
387
388
		$result = $this->connection->restore();
389
390
		if ( is_wp_error( $result ) ) {
391
			$response = $result;
392
		} elseif ( is_string( $result ) ) {
393
			$next = $result;
394
		} else {
395
			$next = true === $result ? 'completed' : 'failed';
396
		}
397
398
		switch ( $next ) {
399
			case 'authorize':
400
				$response['status']       = 'in_progress';
401
				$response['authorizeUrl'] = $this->connection->get_authorization_url();
402
				break;
403
			case 'completed':
404
				$response['status'] = 'completed';
405
				/**
406
				 * Action fired when reconnection has completed successfully.
407
				 *
408
				 * @since 9.0.0
409
				 */
410
				do_action( 'jetpack_reconnection_completed' );
411
				break;
412
			case 'failed':
413
				$response = new WP_Error( 'Reconnect failed' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'Reconnect 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...
414
				break;
415
		}
416
417
		return rest_ensure_response( $response );
418
	}
419
420
	/**
421
	 * Verify that user is allowed to connect Jetpack.
422
	 *
423
	 * @since 9.7.0
424
	 *
425
	 * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
426
	 */
427
	public static function jetpack_register_permission_check() {
428
		if ( current_user_can( 'jetpack_connect' ) ) {
429
			return true;
430
		}
431
432
		return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_user_permission_jetpack_connect'.

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...
433
	}
434
435
	/**
436
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
437
	 *
438
	 * @since 7.7.0
439
	 *
440
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
441
	 *
442
	 * @return \WP_REST_Response|WP_Error
443
	 */
444
	public function connection_register( $request ) {
445 View Code Duplication
		if ( ! wp_verify_nonce( $request->get_param( 'registration_nonce' ), 'jetpack-registration-nonce' ) ) {
446
			return new WP_Error( 'invalid_nonce', __( 'Unable to verify your request.', 'jetpack' ), array( 'status' => 403 ) );
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...
447
		}
448
449
		if ( isset( $request['from'] ) ) {
450
			$this->connection->add_register_request_param( 'from', (string) $request['from'] );
451
		}
452
		$result = $this->connection->try_registration();
453
454
		if ( is_wp_error( $result ) ) {
455
			return $result;
456
		}
457
458
		$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
459
460
		if ( class_exists( 'Jetpack' ) ) {
461
			$authorize_url = \Jetpack::build_authorize_url( $redirect_uri, ! $request->get_param( 'no_iframe' ) );
462
		} else {
463
			if ( ! $request->get_param( 'no_iframe' ) ) {
464
				add_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
465
			}
466
467
			$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
468
469
			if ( ! $request->get_param( 'no_iframe' ) ) {
470
				remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
471
			}
472
		}
473
474
		return rest_ensure_response(
475
			array(
476
				'authorizeUrl' => $authorize_url,
477
			)
478
		);
479
	}
480
481
	/**
482
	 * Get the authorization URL.
483
	 *
484
	 * @since 9.8.0
485
	 *
486
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
487
	 *
488
	 * @return \WP_REST_Response|WP_Error
489
	 */
490
	public function connection_authorize_url( $request ) {
491
		$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
492
493
		if ( ! $request->get_param( 'no_iframe' ) ) {
494
			add_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
495
		}
496
497
		$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
498
499
		if ( ! $request->get_param( 'no_iframe' ) ) {
500
			remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
501
		}
502
503
		return rest_ensure_response(
504
			array(
505
				'authorizeUrl' => $authorize_url,
506
			)
507
		);
508
	}
509
510
}
511