$method, 'timeout' => 20, 'redirection' => 0, 'sslverify' => true, 'headers' => array( 'Accept' => 'application/json' ), ); if ( $token ) { $args['headers']['Authorization'] = 'Bearer ' . $token; } if ( null !== $body ) { $args['headers']['Content-Type'] = $form ? 'application/x-www-form-urlencoded' : 'application/json'; $args['body'] = $form ? http_build_query( $body, '', '&' ) : wp_json_encode( $body ); } $response = wp_remote_request( self::origin() . $path, $args ); if ( is_wp_error( $response ) ) { return new WP_Error( 'overlayqa_network', __( 'OverlayQA could not be reached. Refresh this page before retrying; your last action may have saved.', 'overlayqa' ) ); } $status = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status < 200 || $status >= 300 ) { $messages = array( 401 => __( 'Your OverlayQA connection expired or was revoked. Connect again.', 'overlayqa' ), 403 => __( 'OverlayQA refused this action. Check your project permissions and plan in OverlayQA.', 'overlayqa' ), 404 => __( 'This OverlayQA service or project is unavailable. Check your connection and try again later.', 'overlayqa' ), 429 => __( 'OverlayQA is receiving too many requests. Wait a moment, then try again.', 'overlayqa' ), ); return new WP_Error( 'overlayqa_http', $messages[ $status ] ?? __( 'OverlayQA could not complete this request. Refresh before retrying.', 'overlayqa' ), array( 'status' => $status ) ); } if ( 204 === $status || '' === wp_remote_retrieve_body( $response ) ) { return array(); } return is_array( $data ) ? $data : new WP_Error( 'overlayqa_response', __( 'OverlayQA returned an unexpected response. Please try again.', 'overlayqa' ) ); } /** A database-backed lock serializes refreshes and review creation across tabs. */ public static function lock( $name ) { $key = 'overlayqa_lock_' . hash( 'sha256', $name ); if ( ! add_option( $key, time(), '', false ) ) { // Do not reclaim a live request. A stale lock is cleared for the NEXT attempt. if ( (int) get_option( $key ) < time() - 120 ) { delete_option( $key ); } return new WP_Error( 'overlayqa_busy', __( 'An OverlayQA request is already running. Wait a moment and refresh.', 'overlayqa' ) ); } return $key; } public static function begin_connection() { $callback = self::callback(); $parts = wp_parse_url( $callback ); $local = in_array( $parts['host'] ?? '', array( 'localhost', '127.0.0.1', '[::1]' ), true ); if ( 'https' !== ( $parts['scheme'] ?? '' ) && ! $local ) { return new WP_Error( 'overlayqa_https', __( 'Enable HTTPS for your WordPress admin before connecting OverlayQA.', 'overlayqa' ) ); } $available = self::request( '/api/mcp/oauth/availability' ); if ( is_wp_error( $available ) ) { return $available; } if ( empty( $available['available'] ) ) { return new WP_Error( 'overlayqa_unavailable', __( 'OverlayQA connections are not available yet. Please try again later.', 'overlayqa' ) ); } $registered = self::request( '/mcp/oauth/register', 'POST', array( 'client_name' => 'OverlayQA for WordPress', 'redirect_uris' => array( $callback ), 'token_endpoint_auth_method' => 'none', 'grant_types' => array( 'authorization_code', 'refresh_token' ), 'response_types' => array( 'code' ), 'scope' => self::SCOPES, ) ); if ( is_wp_error( $registered ) ) { return $registered; } if ( empty( $registered['client_id'] ) || ! wp_is_uuid( $registered['client_id'] ) ) { return new WP_Error( 'overlayqa_client', __( 'OverlayQA could not start the connection. Please try again.', 'overlayqa' ) ); } $state = bin2hex( random_bytes( 32 ) ); $verifier = bin2hex( random_bytes( 32 ) ); $pending = array( 'client_id' => $registered['client_id'], 'state' => $state, 'verifier' => $verifier, 'callback' => $callback, 'expires' => time() + 10 * MINUTE_IN_SECONDS, ); update_user_meta( get_current_user_id(), self::key() . '_pending', $pending ); return self::origin() . '/mcp/oauth/authorize?' . http_build_query( array( 'client_id' => $registered['client_id'], 'redirect_uri' => $callback, 'response_type' => 'code', 'resource' => self::origin() . '/mcp', 'scope' => self::SCOPES, 'state' => $state, 'code_challenge' => rtrim( strtr( base64_encode( hash( 'sha256', $verifier, true ) ), '+/', '-_' ), '=' ), 'code_challenge_method' => 'S256', ), '', '&', PHP_QUERY_RFC3986 ); } public static function finish_connection( $state, $issuer, $code, $denied ) { $pending = get_user_meta( get_current_user_id(), self::key() . '_pending', true ); if ( ! is_array( $pending ) || empty( $state ) || ! hash_equals( $pending['state'], $state ) || $pending['expires'] < time() || self::origin() . '/mcp/oauth' !== $issuer || self::callback() !== $pending['callback'] ) { return new WP_Error( 'overlayqa_state', __( 'This connection request is invalid or expired. Start again from OverlayQA settings.', 'overlayqa' ) ); } // Conditional deletion consumes only this user's exact pending request. if ( ! delete_user_meta( get_current_user_id(), self::key() . '_pending', $pending ) ) { return new WP_Error( 'overlayqa_used', __( 'This connection request has already been used.', 'overlayqa' ) ); } if ( $denied || empty( $code ) ) { return new WP_Error( 'overlayqa_cancelled', __( 'Connection cancelled. Your WordPress site was not connected.', 'overlayqa' ) ); } $tokens = self::request( '/mcp/oauth/token', 'POST', array( 'grant_type' => 'authorization_code', 'client_id' => $pending['client_id'], 'resource' => self::origin() . '/mcp', 'code' => $code, 'redirect_uri' => $pending['callback'], 'code_verifier' => $pending['verifier'], ), '', true ); if ( is_wp_error( $tokens ) ) { return $tokens; } return self::store_tokens( $tokens, array( 'client_id' => $pending['client_id'] ) ); } private static function store_tokens( $tokens, $connection ) { if ( empty( $tokens['access_token'] ) || empty( $tokens['refresh_token'] ) || empty( $tokens['expires_in'] ) || ! is_string( $tokens['access_token'] ) || ! is_string( $tokens['refresh_token'] ) ) { return new WP_Error( 'overlayqa_tokens', __( 'OverlayQA could not complete the connection. Please connect again.', 'overlayqa' ) ); } $connection['access_token'] = $tokens['access_token']; $connection['refresh_token'] = $tokens['refresh_token']; $connection['expires'] = time() + (int) $tokens['expires_in']; self::save( $connection ); return true; } public static function api( $path, $method = 'GET', $body = null ) { $connection = self::connection(); if ( empty( $connection['access_token'] ) ) { return new WP_Error( 'overlayqa_disconnected', __( 'Connect your OverlayQA account first.', 'overlayqa' ) ); } if ( $connection['expires'] <= time() + 30 ) { $lock = self::lock( 'refresh-' . get_current_user_id() ); if ( is_wp_error( $lock ) ) { return $lock; } try { $connection = self::connection( true ); if ( $connection['expires'] <= time() + 30 ) { $tokens = self::request( '/mcp/oauth/token', 'POST', array( 'grant_type' => 'refresh_token', 'client_id' => $connection['client_id'], 'resource' => self::origin() . '/mcp', 'refresh_token' => $connection['refresh_token'], ), '', true ); if ( is_wp_error( $tokens ) ) { return $tokens; } $saved = self::store_tokens( $tokens, $connection ); if ( is_wp_error( $saved ) ) { return $saved; } $connection = self::connection(); } } finally { delete_option( $lock ); } } return self::request( '/api/mcp' . $path, $method, $body, $connection['access_token'] ); } public static function disconnect() { $lock = self::lock( 'refresh-' . get_current_user_id() ); if ( is_wp_error( $lock ) ) { return $lock; } try { $connection = self::connection( true ); if ( ! empty( $connection['refresh_token'] ) ) { $result = self::request( '/mcp/oauth/revoke', 'POST', array( 'client_id' => $connection['client_id'], 'token' => $connection['refresh_token'] ), '', true ); if ( is_wp_error( $result ) ) { return new WP_Error( 'overlayqa_revoke', __( 'OverlayQA could not revoke this connection. Try disconnecting again, or revoke it in your OverlayQA account.', 'overlayqa' ) ); } } delete_user_meta( get_current_user_id(), self::key() ); delete_user_meta( get_current_user_id(), self::key() . '_pending' ); return true; } finally { delete_option( $lock ); } } }