<?php
/**
 * Sets up the Connection REST API endpoints.
 *
 * @package automattic/jetpack-connection
 */

namespace Automattic\Jetpack\Connection;

use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Status;
use Jetpack_XMLRPC_Server;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * Registers the REST routes for Connections.
 *
 * @phan-constructor-used-for-side-effects
 */
class REST_Connector {
	/**
	 * The Connection Manager.
	 *
	 * @var Manager
	 */
	private $connection;

	/**
	 * This property stores the localized "Insufficient Permissions" error message.
	 *
	 * @var string Generic error message when user is not allowed to perform an action.
	 */
	private static $user_permissions_error_msg;

	const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
	. 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
	. '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
	. '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
	. 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
	. 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
	. 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
	. 'EwIDAQAB' . "\r\n"
	. '-----END PUBLIC KEY-----' . "\r\n";

	/**
	 * Constructor.
	 *
	 * @param Manager $connection The Connection Manager.
	 */
	public function __construct( Manager $connection ) {
		$this->connection = $connection;

		self::$user_permissions_error_msg = esc_html__(
			'You do not have the correct user permissions to perform this action.
			Please contact your site admin if you think this is a mistake.',
			'jetpack-connection'
		);

		$jp_version = Constants::get_constant( 'JETPACK__VERSION' );

		if ( ! $this->connection->has_connected_owner() ) {
			// Register a site.
			register_rest_route(
				'jetpack/v4',
				'/verify_registration',
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'verify_registration' ),
					'permission_callback' => '__return_true',
				)
			);
		}

		// Authorize a remote user.
		register_rest_route(
			'jetpack/v4',
			'/remote_authorize',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => __CLASS__ . '::remote_authorize',
				'permission_callback' => '__return_true',
			)
		);

		// Authorize a remote user.
		register_rest_route(
			'jetpack/v4',
			'/remote_provision',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'remote_provision' ),
				'permission_callback' => array( $this, 'remote_provision_permission_check' ),
			)
		);

		register_rest_route(
			'jetpack/v4',
			'/remote_register',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'remote_register' ),
				'permission_callback' => array( $this, 'remote_register_permission_check' ),
			)
		);

		// Connect a remote user.
		register_rest_route(
			'jetpack/v4',
			'/remote_connect',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'remote_connect' ),
				'permission_callback' => array( $this, 'remote_connect_permission_check' ),
			)
		);

		// The endpoint verifies blog connection and blog token validity.
		register_rest_route(
			'jetpack/v4',
			'/connection/check',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'connection_check' ),
				'permission_callback' => array( $this, 'connection_check_permission_check' ),
			)
		);

		// Get current connection status of Jetpack.
		register_rest_route(
			'jetpack/v4',
			'/connection',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => __CLASS__ . '::connection_status',
				'permission_callback' => '__return_true',
			)
		);

		// Disconnect site.
		register_rest_route(
			'jetpack/v4',
			'/connection',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => __CLASS__ . '::disconnect_site',
				'permission_callback' => __CLASS__ . '::disconnect_site_permission_check',
				'args'                => array(
					'isActive' => array(
						'description'       => __( 'Set to false will trigger the site to disconnect.', 'jetpack-connection' ),
						'validate_callback' => function ( $value ) {
							if ( false !== $value ) {
								return new WP_Error(
									'rest_invalid_param',
									__( 'The isActive argument should be set to false.', 'jetpack-connection' ),
									array( 'status' => 400 )
								);
							}

							return true;
						},
						'required'          => true,
					),
				),
			)
		);

		// Disconnect/unlink user from WordPress.com servers.
		// this endpoint is set to override the older endpoint that was previously in the Jetpack plugin
		// Override is here in case an older version of the Jetpack plugin is installed alongside an updated standalone.
		register_rest_route(
			'jetpack/v4',
			'/connection/user',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => __CLASS__ . '::unlink_user',
				'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
			),
			true // override other implementations.
		);

		// We are only registering this route if Jetpack-the-plugin is not active or it's version is ge 10.0-alpha.
		// The reason for doing so is to avoid conflicts between the Connection package and
		// older versions of Jetpack, registering the same route twice.
		if ( empty( $jp_version ) || version_compare( $jp_version, '10.0-alpha', '>=' ) ) {
			// Get current user connection data.
			register_rest_route(
				'jetpack/v4',
				'/connection/data',
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => __CLASS__ . '::get_user_connection_data',
					'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
				)
			);
		}

		// Get list of plugins that use the Jetpack connection.
		register_rest_route(
			'jetpack/v4',
			'/connection/plugins',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( __CLASS__, 'get_connection_plugins' ),
				'permission_callback' => __CLASS__ . '::connection_plugins_permission_check',
			)
		);

		// Full or partial reconnect in case of connection issues.
		register_rest_route(
			'jetpack/v4',
			'/connection/reconnect',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'connection_reconnect' ),
				'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check',
			)
		);

		// Register the site (get `blog_token`).
		register_rest_route(
			'jetpack/v4',
			'/connection/register',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'connection_register' ),
				'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
				'args'                => array(
					'from'         => array(
						'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack-connection' ),
						'type'        => 'string',
					),
					'redirect_uri' => array(
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
						'type'        => 'string',
					),
					'plugin_slug'  => array(
						'description' => __( 'Indicates from what plugin the request is coming from', 'jetpack-connection' ),
						'type'        => 'string',
					),
				),
			)
		);

		// Get authorization URL.
		register_rest_route(
			'jetpack/v4',
			'/connection/authorize_url',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'connection_authorize_url' ),
				'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
				'args'                => array(
					'redirect_uri' => array(
						'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
						'type'        => 'string',
					),
				),
			)
		);

		register_rest_route(
			'jetpack/v4',
			'/user-token',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( static::class, 'update_user_token' ),
					'permission_callback' => array( static::class, 'update_user_token_permission_check' ),
					'args'                => array(
						'user_token'          => array(
							'description' => __( 'New user token', 'jetpack-connection' ),
							'type'        => 'string',
							'required'    => true,
						),
						'is_connection_owner' => array(
							'description' => __( 'Is connection owner', 'jetpack-connection' ),
							'type'        => 'boolean',
						),
					),
				),
			)
		);

		// Set the connection owner.
		register_rest_route(
			'jetpack/v4',
			'/connection/owner',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( static::class, 'set_connection_owner' ),
				'permission_callback' => array( static::class, 'set_connection_owner_permission_check' ),
				'args'                => array(
					'owner' => array(
						'description' => __( 'New owner', 'jetpack-connection' ),
						'type'        => 'integer',
						'required'    => true,
					),
				),
			)
		);
	}

	/**
	 * Handles verification that a site is registered.
	 *
	 * @since 1.7.0
	 * @since-jetpack 5.4.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return string|WP_Error
	 */
	public function verify_registration( WP_REST_Request $request ) {
		$registration_data = array( $request['secret_1'], $request['state'] );

		return $this->connection->handle_registration( $registration_data );
	}

	/**
	 * Handles verification that a site is registered
	 *
	 * @since 1.7.0
	 * @since-jetpack 5.4.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return array|WP_Error
	 */
	public static function remote_authorize( $request ) {
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
		$result        = $xmlrpc_server->remote_authorize( $request );

		if ( is_a( $result, 'IXR_Error' ) ) {
			$result = new WP_Error( $result->code, $result->message );
		}

		return $result;
	}

	/**
	 * Initiate the site provisioning process.
	 *
	 * @since 2.5.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return WP_Error|array
	 */
	public function remote_provision( WP_REST_Request $request ) {
		$request_data = $request->get_params();

		if ( current_user_can( 'jetpack_connect_user' ) ) {
			$request_data['local_user'] = get_current_user_id();
		}

		$xmlrpc_server = new Jetpack_XMLRPC_Server();
		$result        = $xmlrpc_server->remote_provision( $request_data );

		if ( is_a( $result, 'IXR_Error' ) ) {
			$result = new WP_Error( $result->code, $result->message );
		}

		return $result;
	}

	/**
	 * Connect a remote user.
	 *
	 * @since 2.6.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return WP_Error|array
	 */
	public static function remote_connect( WP_REST_Request $request ) {
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
		$result        = $xmlrpc_server->remote_connect( $request );

		if ( is_a( $result, 'IXR_Error' ) ) {
			$result = new WP_Error( $result->code, $result->message );
		}

		return $result;
	}

	/**
	 * Register the site so that a plan can be provisioned.
	 *
	 * @since 2.5.0
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|array
	 */
	public function remote_register( WP_REST_Request $request ) {
		$xmlrpc_server = new Jetpack_XMLRPC_Server();
		$result        = $xmlrpc_server->remote_register( $request );

		if ( is_a( $result, 'IXR_Error' ) ) {
			$result = new WP_Error( $result->code, $result->message );
		}

		return $result;
	}

	/**
	 * Remote provision endpoint permission check.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return true|WP_Error
	 */
	public function remote_provision_permission_check( WP_REST_Request $request ) {
		if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
			return true;
		}

		return Rest_Authentication::is_signed_with_blog_token()
			? true
			: new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Remote connect endpoint permission check.
	 *
	 * @return true|WP_Error
	 */
	public function remote_connect_permission_check() {
		return Rest_Authentication::is_signed_with_blog_token()
			? true
			: new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Remote register endpoint permission check.
	 *
	 * @return true|WP_Error
	 */
	public function remote_register_permission_check() {
		if ( $this->connection->has_connected_owner() ) {
			return Rest_Authentication::is_signed_with_blog_token()
				? true
				: new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
		}

		return true;
	}

	/**
	 * Get connection status for this Jetpack site.
	 *
	 * @since 1.7.0
	 * @since-jetpack 4.3.0
	 *
	 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
	 *
	 * @return WP_REST_Response|array Connection information.
	 */
	public static function connection_status( $rest_response = true ) {
		$status     = new Status();
		$connection = new Manager();

		$connection_status = array(
			'isActive'          => $connection->has_connected_owner(), // TODO deprecate this.
			'isStaging'         => $status->in_safe_mode(), // TODO deprecate this.
			'isRegistered'      => $connection->is_connected(),
			'isUserConnected'   => $connection->is_user_connected(),
			'hasConnectedOwner' => $connection->has_connected_owner(),
			'offlineMode'       => array(
				'isActive'        => $status->is_offline_mode(),
				'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
				'url'             => $status->is_local_site(),
				/** This filter is documented in packages/status/src/class-status.php */
				'filter'          => apply_filters( 'jetpack_offline_mode', false ),
				'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
				'option'          => (bool) get_option( 'jetpack_offline_mode' ),
			),
			'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
		);

		/**
		 * Filters the connection status data.
		 *
		 * @since 1.25.0
		 *
		 * @param array An array containing the connection status data.
		 */
		$connection_status = apply_filters( 'jetpack_connection_status', $connection_status );

		if ( $rest_response ) {
			return rest_ensure_response(
				$connection_status
			);
		} else {
			return $connection_status;
		}
	}

	/**
	 * Get plugins connected to the Jetpack.
	 *
	 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
	 *
	 * @since 1.13.1
	 * @since 1.38.0 Added $rest_response param.
	 *
	 * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
	 */
	public static function get_connection_plugins( $rest_response = true ) {
		$plugins = ( new Manager() )->get_connected_plugins();

		if ( is_wp_error( $plugins ) ) {
			return $plugins;
		}

		array_walk(
			$plugins,
			function ( &$data, $slug ) {
				$data['slug'] = $slug;
			}
		);

		if ( $rest_response ) {
			return rest_ensure_response( array_values( $plugins ) );
		}

		return array_values( $plugins );
	}

	/**
	 * Verify that user can view Jetpack admin page and can activate plugins.
	 *
	 * @since 1.15.0
	 *
	 * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
	 */
	public static function activate_plugins_permission_check() {
		if ( current_user_can( 'activate_plugins' ) ) {
			return true;
		}

		return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Permission check for the connection_plugins endpoint
	 *
	 * @return bool|WP_Error
	 */
	public static function connection_plugins_permission_check() {
		if ( true === static::activate_plugins_permission_check() ) {
			return true;
		}

		if ( true === static::is_request_signed_by_jetpack_debugger() ) {
			return true;
		}

		return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Permission check for the disconnect site endpoint.
	 *
	 * @since 1.30.1
	 *
	 * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
	 *
	 * @return bool|WP_Error True if user is able to disconnect the site or the request is signed with a blog token (aka a direct request from WPCOM).
	 */
	public static function disconnect_site_permission_check() {
		if ( current_user_can( 'jetpack_disconnect' ) ) {
			return true;
		}

		return Rest_Authentication::is_signed_with_blog_token()
			? true
			: new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
	 *
	 * @since 6.3.3
	 *
	 * @return bool|WP_Error True if user is able to unlink.
	 */
	public static function unlink_user_permission_callback() {
		// This is a mapped capability
		// phpcs:ignore WordPress.WP.Capabilities.Unknown
		if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
			return true;
		}

		return new WP_Error(
			'invalid_user_permission_unlink_user',
			self::get_user_permissions_error_msg(),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
	 * Information about the master/primary user.
	 * Information about the current user.
	 *
	 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
	 *
	 * @since 1.30.1
	 *
	 * @return \WP_REST_Response|array
	 */
	public static function get_user_connection_data( $rest_response = true ) {
		$blog_id = \Jetpack_Options::get_option( 'id' );

		$connection = new Manager();

		$current_user     = wp_get_current_user();
		$connection_owner = $connection->get_connection_owner();

		$owner_display_name = false === $connection_owner ? null : $connection_owner->display_name;

		$is_user_connected = $connection->is_user_connected();
		$is_master_user    = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID );
		$wpcom_user_data   = $connection->get_connected_user_data();

		// Add connected user gravatar to the returned wpcom_user_data.
		// Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so
		// clients probably expect that by now.
		if ( false === $wpcom_user_data ) {
			$wpcom_user_data = array();
		}
		$wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ?
		get_avatar_url(
			$wpcom_user_data['email'],
			array(
				'size'    => 64,
				'default' => 'mysteryman',
			)
		)
		: false );

		// Check for possible account errors between the local user and WPCOM account.
		$possible_errors = array();
		if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) {
			$user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status();
			$possible_errors     = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] );
		}

		$current_user_connection_data = array(
			'isConnected'           => $is_user_connected,
			'isMaster'              => $is_master_user,
			'username'              => $current_user->user_login,
			'id'                    => $current_user->ID,
			'blogId'                => $blog_id,
			'wpcomUser'             => $wpcom_user_data,
			'gravatar'              => get_avatar_url( $current_user->ID ),
			'permissions'           => array(
				'connect'        => current_user_can( 'jetpack_connect' ),
				'connect_user'   => current_user_can( 'jetpack_connect_user' ),
				// This is a mapped capability
				// phpcs:ignore WordPress.WP.Capabilities.Unknown
				'unlink_user'    => current_user_can( 'jetpack_unlink_user' ),
				'disconnect'     => current_user_can( 'jetpack_disconnect' ),
				'manage_options' => current_user_can( 'manage_options' ),
			),
			'possibleAccountErrors' => $possible_errors,
		);

		/**
		 * Filters the current user connection data.
		 *
		 * @since 1.30.1
		 *
		 * @param array An array containing the current user connection data.
		 */
		$current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data );

		$response = array(
			'currentUser'     => $current_user_connection_data,
			'connectionOwner' => $owner_display_name,
			'isRegistered'    => $connection->is_connected(),
		);

		if ( $rest_response ) {
			return rest_ensure_response( $response );
		}

		return $response;
	}

	/**
	 * Verify that user is allowed to disconnect Jetpack.
	 *
	 * @since 1.15.0
	 *
	 * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
	 */
	public static function jetpack_reconnect_permission_check() {
		if ( current_user_can( 'jetpack_reconnect' ) ) {
			return true;
		}

		return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Returns generic error message when user is not allowed to perform an action.
	 *
	 * @return string The error message.
	 */
	public static function get_user_permissions_error_msg() {
		return self::$user_permissions_error_msg;
	}

	/**
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
	 *
	 * @since 1.15.0
	 *
	 * @return \WP_REST_Response|WP_Error
	 */
	public function connection_reconnect() {
		$response = array();

		$next = null;

		$result = $this->connection->restore();

		if ( is_wp_error( $result ) ) {
			$response = $result;
		} elseif ( is_string( $result ) ) {
			$next = $result;
		} else {
			$next = true === $result ? 'completed' : 'failed';
		}

		switch ( $next ) {
			case 'authorize':
				$response['status']       = 'in_progress';
				$response['authorizeUrl'] = $this->connection->get_authorization_url();
				break;
			case 'completed':
				$response['status'] = 'completed';
				/**
				 * Action fired when reconnection has completed successfully.
				 *
				 * @since 1.18.1
				 */
				do_action( 'jetpack_reconnection_completed' );
				break;
			case 'failed':
				$response = new WP_Error( 'Reconnect failed' );
				break;
		}

		return rest_ensure_response( $response );
	}

	/**
	 * Verify that user is allowed to connect Jetpack.
	 *
	 * @since 1.26.0
	 *
	 * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
	 */
	public static function jetpack_register_permission_check() {
		if ( current_user_can( 'jetpack_connect' ) ) {
			return true;
		}

		return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * The endpoint tried to connect Jetpack site to WPCOM.
	 *
	 * @since 1.7.0
	 * @since 6.7.0 No longer needs `registration_nonce`.
	 * @since-jetpack 7.7.0
	 *
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return \WP_REST_Response|WP_Error
	 */
	public function connection_register( $request ) {
		if ( isset( $request['from'] ) ) {
			$this->connection->add_register_request_param( 'from', (string) $request['from'] );
		}

		if ( ! empty( $request['plugin_slug'] ) ) {
			// If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
			$connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
			if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
				$this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
			}
		}

		$result = $this->connection->try_registration();

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;

		$authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri );

		/**
		 * Filters the response of jetpack/v4/connection/register endpoint
		 *
		 * @param array $response Array response
		 * @since 1.27.0
		 */
		$response_body = apply_filters(
			'jetpack_register_site_rest_response',
			array()
		);

		// We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
		$response_body['authorizeUrl'] = $authorize_url;
		if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
			$response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
		}

		return rest_ensure_response( $response_body );
	}

	/**
	 * Get the authorization URL.
	 *
	 * @since 1.27.0
	 *
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return \WP_REST_Response|WP_Error
	 */
	public function connection_authorize_url( $request ) {
		$redirect_uri  = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
		$authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );

		return rest_ensure_response(
			array(
				'authorizeUrl' => $authorize_url,
			)
		);
	}

	/**
	 * The endpoint tried to partially or fully reconnect the website to WP.com.
	 *
	 * @since 1.29.0
	 *
	 * @param \WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return \WP_REST_Response|WP_Error
	 */
	public static function update_user_token( $request ) {
		$token_parts = explode( '.', $request['user_token'] );

		if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
			return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
		}

		$user_id = (int) $token_parts[2];

		if ( false === get_userdata( $user_id ) ) {
			return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
		}

		$connection = new Manager();

		if ( ! $connection->is_connected() ) {
			return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
		}

		$is_connection_owner = isset( $request['is_connection_owner'] )
			? (bool) $request['is_connection_owner']
			: ( new Manager() )->get_connection_owner_id() === $user_id;

		( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );

		/**
		 * Fires when the user token gets successfully replaced.
		 *
		 * @since 1.29.0
		 *
		 * @param int $user_id User ID.
		 * @param string $token New user token.
		 */
		do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );

		return rest_ensure_response(
			array(
				'success' => true,
			)
		);
	}

	/**
	 * Disconnects Jetpack from the WordPress.com Servers
	 *
	 * @since 1.30.1
	 *
	 * @return bool|WP_Error True if Jetpack successfully disconnected.
	 */
	public static function disconnect_site() {
		$connection = new Manager();

		if ( $connection->is_connected() ) {
			$connection->disconnect_site();
			return rest_ensure_response( array( 'code' => 'success' ) );
		}

		return new WP_Error(
			'disconnect_failed',
			esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
			array( 'status' => 400 )
		);
	}

	/**
	 * Unlinks current user from the WordPress.com Servers.
	 *
	 * @since 6.3.3
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return bool|WP_Error True if user successfully unlinked.
	 */
	public static function unlink_user( $request ) {

		if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
			return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
		}

		// If the user is also connection owner, we need to disconnect all users. Since disconnecting all users is a destructive action, we need to pass a parameter to confirm the action.
		$disconnect_all_users = false;

		if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
			if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
				$disconnect_all_users = true;
			} else {
				return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
			}
		}

		// Allow admins to force a disconnect by passing the "force" parameter
		// This allows an admin to disconnect themselves
		if ( isset( $request['force'] ) && false !== $request['force'] && current_user_can( 'manage_options' ) && ( new Manager( 'jetpack' ) )->disconnect_user_force( get_current_user_id(), $disconnect_all_users ) ) {
			return rest_ensure_response(
				array(
					'code' => 'success',
				)
			);
		} elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
			return rest_ensure_response(
				array(
					'code' => 'success',
				)
			);
		}

		return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
	}

	/**
	 * Verify that the API client is allowed to replace user token.
	 *
	 * @since 1.29.0
	 *
	 * @return bool|WP_Error
	 */
	public static function update_user_token_permission_check() {
		return Rest_Authentication::is_signed_with_blog_token()
			? true
			: new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Change the connection owner.
	 *
	 * @since 1.29.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return \WP_REST_Response|WP_Error
	 */
	public static function set_connection_owner( $request ) {
		$new_owner_id = $request['owner'];

		$owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );

		if ( is_wp_error( $owner_set ) ) {
			return $owner_set;
		}

		return rest_ensure_response(
			array(
				'code' => 'success',
			)
		);
	}

	/**
	 * Check that user has permission to change the master user.
	 *
	 * @since 1.7.0
	 * @since-jetpack 6.2.0
	 * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
	 *
	 * @return bool|WP_Error True if user is able to change master user.
	 */
	public static function set_connection_owner_permission_check() {
		if ( current_user_can( 'jetpack_disconnect' ) ) {
			return true;
		}

		return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * The endpoint verifies blog connection and blog token validity.
	 *
	 * @since 2.7.0
	 *
	 * @return mixed|null
	 */
	public function connection_check() {
		/**
		 * Filters the successful response of the REST API test_connection method
		 *
		 * @param string $response The response string.
		 */
		$status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );

		return rest_ensure_response(
			array(
				'status' => $status,
			)
		);
	}

	/**
	 * Remote connect endpoint permission check.
	 *
	 * @return true|WP_Error
	 */
	public function connection_check_permission_check() {
		if ( current_user_can( 'jetpack_connect' ) ) {
			return true;
		}

		return Rest_Authentication::is_signed_with_blog_token()
			? true
			: new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Permission check for the connection/data endpoint
	 *
	 * @return bool|WP_Error
	 */
	public static function user_connection_data_permission_check() {
		if ( current_user_can( 'jetpack_connect_user' ) ) {
			return true;
		}

		return new WP_Error(
			'invalid_user_permission_user_connection_data',
			self::get_user_permissions_error_msg(),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Verifies if the request was signed with the Jetpack Debugger key
	 *
	 * @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.
	 *
	 * @return bool
	 */
	public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
		 // phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
			return false;
		}

		// signature timestamp must be within 5min of current time.
		if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
			return false;
		}

		$signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode

		$signature_data = wp_json_encode(
			array(
				'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
				'timestamp'  => (int) $_GET['timestamp'],
				'url'        => filter_var( wp_unslash( $_GET['url'] ) ),
			),
			0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
		);

		if (
			! function_exists( 'openssl_verify' )
			|| 1 !== openssl_verify(
				$signature_data,
				$signature,
				$pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
			)
		) {
			return false;
		}

		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		return true;
	}
}
