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', ) ); // 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, ), ), ) ); // 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', ), 'registration_nonce' => array( 'description' => __( 'The registration nonce', 'jetpack-connection' ), 'type' => 'string', 'required' => true, ), '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; } /** * 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->is_staging_site(), '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_development_mode', false ) || apply_filters( 'jetpack_offline_mode', false ) ), // jetpack_development_mode is deprecated. 'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV, ), '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 * * @return bool|WP_Error True if user is able to disconnect the site. */ public static function disconnect_site_permission_check() { if ( current_user_can( 'jetpack_disconnect' ) ) { return true; } return new WP_Error( 'invalid_user_permission_jetpack_disconnect', 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 ) { $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 ); $current_user_connection_data = array( 'isConnected' => $is_user_connected, 'isMaster' => $is_master_user, 'username' => $current_user->user_login, 'id' => $current_user->ID, 'wpcomUser' => $wpcom_user_data, 'gravatar' => get_avatar_url( $current_user->ID, 64, 'mm', '', array( 'force_display' => true ) ), 'permissions' => array( 'connect' => current_user_can( 'jetpack_connect' ), 'connect_user' => current_user_can( 'jetpack_connect_user' ), 'disconnect' => current_user_can( 'jetpack_disconnect' ), ), ); /** * 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, ); if ( $rest_response ) { return rest_ensure_response( $response ); } return $response; } /** * 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'], $_GET['timestamp'], $_GET['url'], $_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'] ) ), ) ); 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; } /** * 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 partially or fully reconnect the website to WP.com. * * @since 1.7.0 * @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 ( ! wp_verify_nonce( $request->get_param( 'registration_nonce' ), 'jetpack-registration-nonce' ) ) { return new WP_Error( 'invalid_nonce', __( 'Unable to verify your request.', 'jetpack-connection' ), array( 'status' => 403 ) ); } 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; if ( class_exists( 'Jetpack' ) ) { $authorize_url = \Jetpack::build_authorize_url( $redirect_uri ); } else { $authorize_url = $this->connection->get_authorization_url( null, $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 can not 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 ) ); } /** * 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() ) ); } }