*/ private array $s; public function __construct( ?array $settings = null ) { $this->s = $settings ?? Vimine_Settings::get(); } public static function pkce_verifier(): string { return rtrim( strtr( base64_encode( random_bytes( 32 ) ), '+/', '-_' ), '=' ); } public static function pkce_challenge( string $verifier ): string { return rtrim( strtr( base64_encode( hash( 'sha256', $verifier, true ) ), '+/', '-_' ), '=' ); } /** The hosted sign-in URL the browser is sent to. */ public function authorize_url( string $state, string $challenge, ?string $scopes = null, ?string $prompt = null ): string { $q = [ 'client_id' => $this->s['client_id'], 'redirect_uri' => Vimine_Settings::redirect_uri(), 'response_type' => 'code', 'state' => $state, 'code_challenge' => $challenge, 'code_challenge_method' => 'S256', 'scope' => $scopes ?? $this->s['scopes'], ]; if ( $prompt ) { $q['prompt'] = $prompt; } return $this->s['app_url'] . '/oauth/authorize?' . http_build_query( $q, '', '&', PHP_QUERY_RFC3986 ); } /** @return array|WP_Error token response */ public function exchange_code( string $code, string $verifier ) { return $this->token( [ 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => Vimine_Settings::redirect_uri(), 'code_verifier' => $verifier, ] ); } /** @return array|WP_Error */ public function refresh( string $refresh_token ) { return $this->token( [ 'grant_type' => 'refresh_token', 'refresh_token' => $refresh_token ] ); } /** @return array|WP_Error */ public function client_credentials( ?string $scopes = null ) { $body = [ 'grant_type' => 'client_credentials' ]; if ( $scopes ) { $body['scope'] = $scopes; } return $this->token( $body ); } public function revoke( string $token ): void { wp_remote_post( $this->s['api_url'] . '/v1/oauth/revoke', [ 'timeout' => 10, 'headers' => [ 'Authorization' => $this->basic() ], 'body' => [ 'token' => $token ], ] ); } /** RP-initiated logout target on the app host (ends the Vimine session too). */ public function logout_url( string $post_logout_redirect_uri ): string { return $this->s['app_url'] . '/oauth/logout?' . http_build_query( [ 'client_id' => $this->s['client_id'], 'post_logout_redirect_uri' => $post_logout_redirect_uri ], '', '&', PHP_QUERY_RFC3986 ); } /** * An authenticated API call. `$accept` = 'json' (default) or a raw media type (e.g. image/svg+xml). * * @return array|string|WP_Error decoded JSON, raw body for non-JSON accepts, or WP_Error(code) */ public function request( string $method, string $path, string $access_token, ?array $json = null, string $accept = 'json' ) { $args = [ 'method' => $method, 'timeout' => 15, 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Accept' => 'json' === $accept ? 'application/json' : $accept, ], ]; if ( null !== $json ) { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( $json ); } $res = wp_remote_request( $this->s['api_url'] . $path, $args ); if ( is_wp_error( $res ) ) { return new WP_Error( 'network', $res->get_error_message() ); } $status = (int) wp_remote_retrieve_response_code( $res ); $body = (string) wp_remote_retrieve_body( $res ); if ( $status >= 400 ) { $problem = json_decode( $body, true ); return new WP_Error( is_array( $problem ) && ! empty( $problem['code'] ) ? $problem['code'] : 'http_' . $status, is_array( $problem ) ? ( $problem['detail'] ?? $problem['title'] ?? '' ) : '', [ 'status' => $status ] ); } if ( 'json' !== $accept ) { return $body; } $data = json_decode( $body, true ); return is_array( $data ) ? $data : []; } /** * A public (no credentials) GET — the association's public profile and PUBLIC events. * * @return array|WP_Error */ public function public_get( string $path ) { $res = wp_remote_get( $this->s['api_url'] . $path, [ 'timeout' => 15, 'headers' => [ 'Accept' => 'application/json' ] ] ); if ( is_wp_error( $res ) ) { return new WP_Error( 'network', $res->get_error_message() ); } $status = (int) wp_remote_retrieve_response_code( $res ); $data = json_decode( (string) wp_remote_retrieve_body( $res ), true ); if ( $status >= 400 ) { return new WP_Error( is_array( $data ) && ! empty( $data['code'] ) ? $data['code'] : 'http_' . $status, is_array( $data ) ? ( $data['detail'] ?? $data['title'] ?? '' ) : '', [ 'status' => $status ] ); } return is_array( $data ) ? $data : []; } private function basic(): string { return 'Basic ' . base64_encode( $this->s['client_id'] . ':' . $this->s['client_secret'] ); } /** @return array|WP_Error */ private function token( array $form ) { $res = wp_remote_post( $this->s['api_url'] . '/v1/oauth/token', [ 'timeout' => 15, 'headers' => [ 'Authorization' => $this->basic(), 'Accept' => 'application/json' ], 'body' => $form, ] ); if ( is_wp_error( $res ) ) { return new WP_Error( 'network', $res->get_error_message() ); } $data = json_decode( (string) wp_remote_retrieve_body( $res ), true ); if ( (int) wp_remote_retrieve_response_code( $res ) >= 400 || ! is_array( $data ) || empty( $data['access_token'] ) ) { return new WP_Error( is_array( $data ) && ! empty( $data['error'] ) ? $data['error'] : 'token_error', is_array( $data ) ? ( $data['error_description'] ?? '' ) : '' ); } return $data; } }