Completed
Push — add/testing-info ( be1095...03b7e9 )
by
unknown
09:20
created

activate_plugins_permission_check()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 7
rs 10
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\Redirect;
11
use Automattic\Jetpack\Status;
12
use Jetpack_XMLRPC_Server;
13
use WP_Error;
14
use WP_REST_Request;
15
use WP_REST_Response;
16
use WP_REST_Server;
17
18
/**
19
 * Registers the REST routes for Connections.
20
 */
21
class REST_Connector {
22
	/**
23
	 * The Connection Manager.
24
	 *
25
	 * @var Manager
26
	 */
27
	private $connection;
28
29
	/**
30
	 * This property stores the localized "Insufficient Permissions" error message.
31
	 *
32
	 * @var string Generic error message when user is not allowed to perform an action.
33
	 */
34
	private static $user_permissions_error_msg;
35
36
	const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
37
	. 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
38
	. '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
39
	. '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
40
	. 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
41
	. 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
42
	. 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
43
	. 'EwIDAQAB' . "\r\n"
44
	. '-----END PUBLIC KEY-----' . "\r\n";
45
46
	/**
47
	 * Constructor.
48
	 *
49
	 * @param Manager $connection The Connection Manager.
50
	 */
51
	public function __construct( Manager $connection ) {
52
		$this->connection = $connection;
53
54
		self::$user_permissions_error_msg = esc_html__(
55
			'You do not have the correct user permissions to perform this action.
56
			Please contact your site admin if you think this is a mistake.',
57
			'jetpack'
58
		);
59
60
		if ( ! $this->connection->has_connected_owner() ) {
61
			// Register a site.
62
			register_rest_route(
63
				'jetpack/v4',
64
				'/verify_registration',
65
				array(
66
					'methods'             => WP_REST_Server::EDITABLE,
67
					'callback'            => array( $this, 'verify_registration' ),
68
					'permission_callback' => '__return_true',
69
				)
70
			);
71
		}
72
73
		// Authorize a remote user.
74
		register_rest_route(
75
			'jetpack/v4',
76
			'/remote_authorize',
77
			array(
78
				'methods'             => WP_REST_Server::EDITABLE,
79
				'callback'            => __CLASS__ . '::remote_authorize',
80
				'permission_callback' => '__return_true',
81
			)
82
		);
83
84
		// Get current connection status of Jetpack.
85
		register_rest_route(
86
			'jetpack/v4',
87
			'/connection',
88
			array(
89
				'methods'             => WP_REST_Server::READABLE,
90
				'callback'            => __CLASS__ . '::connection_status',
91
				'permission_callback' => '__return_true',
92
			)
93
		);
94
95
		// Get list of plugins that use the Jetpack connection.
96
		register_rest_route(
97
			'jetpack/v4',
98
			'/connection/plugins',
99
			array(
100
				'methods'             => WP_REST_Server::READABLE,
101
				'callback'            => array( $this, 'get_connection_plugins' ),
102
				'permission_callback' => __CLASS__ . '::connection_plugins_permission_check',
103
			)
104
		);
105
106
		// Full or partial reconnect in case of connection issues.
107
		register_rest_route(
108
			'jetpack/v4',
109
			'/connection/reconnect',
110
			array(
111
				'methods'             => WP_REST_Server::EDITABLE,
112
				'callback'            => array( $this, 'connection_reconnect' ),
113
				'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check',
114
			)
115
		);
116
117
		// Register the site (get `blog_token`).
118
		register_rest_route(
119
			'jetpack/v4',
120
			'/connection/register',
121
			array(
122
				'methods'             => WP_REST_Server::EDITABLE,
123
				'callback'            => array( $this, 'connection_register' ),
124
				'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
125
				'args'                => array(
126
					'from'               => array(
127
						'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack' ),
128
						'type'        => 'string',
129
					),
130
					'registration_nonce' => array(
131
						'description' => __( 'The registration nonce', 'jetpack' ),
132
						'type'        => 'string',
133
						'required'    => true,
134
					),
135
					'no_iframe'          => array(
136
						'description' => __( 'Disable In-Place connection flow and go straight to Calypso', 'jetpack' ),
137
						'type'        => 'boolean',
138
					),
139
					'redirect_uri'       => array(
140
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack' ),
141
						'type'        => 'string',
142
					),
143
				),
144
			)
145
		);
146
147
		// Get authorization URL.
148
		register_rest_route(
149
			'jetpack/v4',
150
			'/connection/authorize_url',
151
			array(
152
				'methods'             => WP_REST_Server::READABLE,
153
				'callback'            => array( $this, 'connection_authorize_url' ),
154
				'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
155
				'args'                => array(
156
					'no_iframe'    => array(
157
						'description' => __( 'Disable In-Place connection flow and go straight to Calypso', 'jetpack' ),
158
						'type'        => 'boolean',
159
					),
160
					'redirect_uri' => array(
161
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack' ),
162
						'type'        => 'string',
163
					),
164
				),
165
			)
166
		);
167
	}
168
169
	/**
170
	 * Handles verification that a site is registered.
171
	 *
172
	 * @since 5.4.0
173
	 *
174
	 * @param WP_REST_Request $request The request sent to the WP REST API.
175
	 *
176
	 * @return string|WP_Error
177
	 */
178
	public function verify_registration( WP_REST_Request $request ) {
179
		$registration_data = array( $request['secret_1'], $request['state'] );
180
181
		return $this->connection->handle_registration( $registration_data );
182
	}
183
184
	/**
185
	 * Handles verification that a site is registered
186
	 *
187
	 * @since 5.4.0
188
	 *
189
	 * @param WP_REST_Request $request The request sent to the WP REST API.
190
	 *
191
	 * @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...
192
	 */
193
	public static function remote_authorize( $request ) {
194
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
195
		$result        = $xmlrpc_server->remote_authorize( $request );
196
197
		if ( is_a( $result, 'IXR_Error' ) ) {
198
			$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...
199
		}
200
201
		return $result;
202
	}
203
204
	/**
205
	 * Get connection status for this Jetpack site.
206
	 *
207
	 * @since 4.3.0
208
	 *
209
	 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
210
	 *
211
	 * @return WP_REST_Response|array Connection information.
212
	 */
213
	public static function connection_status( $rest_response = true ) {
214
		$status     = new Status();
215
		$connection = new Manager();
216
217
		$connection_status = array(
218
			'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...
219
			'isStaging'         => $status->is_staging_site(),
220
			'isRegistered'      => $connection->is_connected(),
221
			'isUserConnected'   => $connection->is_user_connected(),
222
			'hasConnectedOwner' => $connection->has_connected_owner(),
223
			'offlineMode'       => array(
224
				'isActive'        => $status->is_offline_mode(),
225
				'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
226
				'url'             => $status->is_local_site(),
227
				/** This filter is documented in packages/status/src/class-status.php */
228
				'filter'          => ( apply_filters( 'jetpack_development_mode', false ) || apply_filters( 'jetpack_offline_mode', false ) ), // jetpack_development_mode is deprecated.
229
				'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
230
			),
231
			'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
232
		);
233
234
		/**
235
		 * Filters the connection status data.
236
		 *
237
		 * @since 9.6.0
238
		 *
239
		 * @param array An array containing the connection status data.
240
		 */
241
		$connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
242
243
		if ( $rest_response ) {
244
			return rest_ensure_response(
245
				$connection_status
246
			);
247
		} else {
248
			return $connection_status;
249
		}
250
	}
251
252
	/**
253
	 * Get plugins connected to the Jetpack.
254
	 *
255
	 * @since 8.6.0
256
	 *
257
	 * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
258
	 */
259
	public function get_connection_plugins() {
260
		$plugins = $this->connection->get_connected_plugins();
261
262
		if ( is_wp_error( $plugins ) ) {
263
			return $plugins;
264
		}
265
266
		array_walk(
267
			$plugins,
268
			function ( &$data, $slug ) {
269
				$data['slug'] = $slug;
270
			}
271
		);
272
273
		return rest_ensure_response( array_values( $plugins ) );
274
	}
275
276
	/**
277
	 * Verify that user can view Jetpack admin page and can activate plugins.
278
	 *
279
	 * @since 8.8.0
280
	 *
281
	 * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
282
	 */
283
	public static function activate_plugins_permission_check() {
284
		if ( current_user_can( 'activate_plugins' ) ) {
285
			return true;
286
		}
287
288
		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...
289
	}
290
291
	/**
292
	 * Permission check for the connection_plugins endpoint
293
	 *
294
	 * @return bool|WP_Error
295
	 */
296
	public static function connection_plugins_permission_check() {
297
		if ( true === static::activate_plugins_permission_check() ) {
298
			return true;
299
		}
300
301
		if ( true === static::is_request_signed_by_jetpack_debugger() ) {
302
			return true;
303
		}
304
305
		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...
306
307
	}
308
309
	/**
310
	 * Verifies if the request was signed with the Jetpack Debugger key
311
	 *
312
	 * @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.
313
	 *
314
	 * @return bool
315
	 */
316
	public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
317
		 // phpcs:disable WordPress.Security.NonceVerification.Recommended
318
		if ( ! isset( $_GET['signature'], $_GET['timestamp'], $_GET['url'], $_GET['rest_route'] ) ) {
319
			return false;
320
		}
321
322
		// signature timestamp must be within 5min of current time.
323
		if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
324
			return false;
325
		}
326
327
		$signature = base64_decode( $_GET['signature'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
328
329
		$signature_data = wp_json_encode(
330
			array(
331
				'rest_route' => $_GET['rest_route'],
332
				'timestamp'  => (int) $_GET['timestamp'],
333
				'url'        => wp_unslash( $_GET['url'] ),
334
			)
335
		);
336
337
		if (
338
			! function_exists( 'openssl_verify' )
339
			|| 1 !== openssl_verify(
340
				$signature_data,
341
				$signature,
342
				$pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
343
			)
344
		) {
345
			return false;
346
		}
347
348
		// phpcs:enable WordPress.Security.NonceVerification.Recommended
349
350
		return true;
351
	}
352
353
	/**
354
	 * Verify that user is allowed to disconnect Jetpack.
355
	 *
356
	 * @since 8.8.0
357
	 *
358
	 * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
359
	 */
360
	public static function jetpack_reconnect_permission_check() {
361
		if ( current_user_can( 'jetpack_reconnect' ) ) {
362
			return true;
363
		}
364
365
		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...
366
	}
367
368
	/**
369
	 * Returns generic error message when user is not allowed to perform an action.
370
	 *
371
	 * @return string The error message.
372
	 */
373
	public static function get_user_permissions_error_msg() {
374
		return self::$user_permissions_error_msg;
375
	}
376
377
	/**
378
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
379
	 *
380
	 * @since 8.8.0
381
	 *
382
	 * @return \WP_REST_Response|WP_Error
383
	 */
384
	public function connection_reconnect() {
385
		$response = array();
386
387
		$next = null;
388
389
		$result = $this->connection->restore();
390
391
		if ( is_wp_error( $result ) ) {
392
			$response = $result;
393
		} elseif ( is_string( $result ) ) {
394
			$next = $result;
395
		} else {
396
			$next = true === $result ? 'completed' : 'failed';
397
		}
398
399
		switch ( $next ) {
400
			case 'authorize':
401
				$response['status']       = 'in_progress';
402
				$response['authorizeUrl'] = $this->connection->get_authorization_url();
403
				break;
404
			case 'completed':
405
				$response['status'] = 'completed';
406
				/**
407
				 * Action fired when reconnection has completed successfully.
408
				 *
409
				 * @since 9.0.0
410
				 */
411
				do_action( 'jetpack_reconnection_completed' );
412
				break;
413
			case 'failed':
414
				$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...
415
				break;
416
		}
417
418
		return rest_ensure_response( $response );
419
	}
420
421
	/**
422
	 * Verify that user is allowed to connect Jetpack.
423
	 *
424
	 * @since 9.7.0
425
	 *
426
	 * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
427
	 */
428
	public static function jetpack_register_permission_check() {
429
		if ( current_user_can( 'jetpack_connect' ) ) {
430
			return true;
431
		}
432
433
		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...
434
	}
435
436
	/**
437
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
438
	 *
439
	 * @since 7.7.0
440
	 *
441
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
442
	 *
443
	 * @return \WP_REST_Response|WP_Error
444
	 */
445
	public function connection_register( $request ) {
446 View Code Duplication
		if ( ! wp_verify_nonce( $request->get_param( 'registration_nonce' ), 'jetpack-registration-nonce' ) ) {
447
			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...
448
		}
449
450
		if ( isset( $request['from'] ) ) {
451
			$this->connection->add_register_request_param( 'from', (string) $request['from'] );
452
		}
453
		$result = $this->connection->try_registration();
454
455
		if ( is_wp_error( $result ) ) {
456
			return $result;
457
		}
458
459
		$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
460
461
		if ( class_exists( 'Jetpack' ) ) {
462
			$authorize_url = \Jetpack::build_authorize_url( $redirect_uri, ! $request->get_param( 'no_iframe' ) );
463
		} else {
464
			if ( ! $request->get_param( 'no_iframe' ) ) {
465
				add_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
466
			}
467
468
			$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
469
470
			if ( ! $request->get_param( 'no_iframe' ) ) {
471
				remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
472
			}
473
		}
474
475
		/**
476
		 * Filters the response of jetpack/v4/connection/register endpoint
477
		 *
478
		 * @param array $response Array response
479
		 * @since 9.8.0
480
		 */
481
		$response_body = apply_filters(
482
			'jetpack_register_site_rest_response',
483
			array()
484
		);
485
486
		// We manipulate the alternate URLs after the filter is applied, so they can not be overwritten.
487
		$response_body['authorizeUrl'] = $authorize_url;
488
		if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
489
			$response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
490
		}
491
492
		return rest_ensure_response( $response_body );
493
	}
494
495
	/**
496
	 * Get the authorization URL.
497
	 *
498
	 * @since 9.8.0
499
	 *
500
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
501
	 *
502
	 * @return \WP_REST_Response|WP_Error
503
	 */
504
	public function connection_authorize_url( $request ) {
505
		$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
506
507
		if ( ! $request->get_param( 'no_iframe' ) ) {
508
			add_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
509
		}
510
511
		$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
512
513
		if ( ! $request->get_param( 'no_iframe' ) ) {
514
			remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_true' );
515
		}
516
517
		return rest_ensure_response(
518
			array(
519
				'authorizeUrl' => $authorize_url,
520
			)
521
		);
522
	}
523
524
}
525