options = $options; } /** * Exchange an authorization code for a site token (public endpoint). * * @param string $code Single-use authorization code. * @param string $site_url Site URL the code was issued for. * @param string $state CSRF state value. * @return array|WP_Error Array with integration_id, site_token, site_url. */ public function complete( $code, $site_url, $state ) { return $this->post( '/api/v1/integrations/complete', array( 'code' => $code, 'siteUrl' => $site_url, 'state' => $state, ), '' ); } /** * Create/select the Umami website for an integration. * * @param string $token Site token. * @param string $integration_id Integration UUID. * @param string $site_name Optional human-readable site name. * @return array|WP_Error */ public function register_website( $token, $integration_id, $site_name = '' ) { return $this->post( '/api/v1/integrations/' . $integration_id . '/websites', array( 'siteName' => $site_name, ), $token ); } /** * Get connection status. * * @param string $token Site token. * @param string $integration_id Integration UUID. * @return array|WP_Error */ public function get_status( $token, $integration_id ) { return $this->get( '/api/v1/integrations/' . $integration_id . '/status', $token ); } /** * Get tracking configuration (script URL + website id). * * @param string $token Site token. * @param string $integration_id Integration UUID. * @return array|WP_Error */ public function get_config( $token, $integration_id ) { return $this->get( '/api/v1/integrations/' . $integration_id . '/config', $token ); } /** * Disconnect and permanently delete the Umami website and its analytics data. * * The backend requires the exact typed confirmation "DELETE"; without it * the request is rejected and nothing is deleted. * * @param string $token Site token. * @param string $integration_id Integration UUID. * @param string $confirmation Typed confirmation, must be "DELETE". * @return array|WP_Error */ public function disconnect( $token, $integration_id, $confirmation = '' ) { return $this->post( '/api/v1/integrations/' . $integration_id . '/disconnect', array( 'confirmation' => $confirmation ), $token ); } /** * POST helper with one retry on transport failure. * * @param string $path API path. * @param array $body JSON body. * @param string $token Optional site token. * @return array|WP_Error */ private function post( $path, array $body, $token ) { $response = $this->request_with_retry( $this->url( $path ), array( 'method' => 'POST', 'timeout' => self::TIMEOUT_SECONDS, 'headers' => $this->headers( $token ), 'body' => wp_json_encode( $body ), ) ); return $this->parse( $response ); } /** * GET helper with one retry on transport failure. * * @param string $path API path. * @param string $token Site token. * @return array|WP_Error */ private function get( $path, $token ) { $response = $this->request_with_retry( $this->url( $path ), array( 'method' => 'GET', 'timeout' => self::TIMEOUT_SECONDS, 'headers' => $this->headers( $token ), ) ); return $this->parse( $response ); } /** * Perform the request, retrying once on transport-level failure. * * @param string $url Full URL. * @param array $args wp_remote_* args. * @return array|WP_Error */ private function request_with_retry( $url, array $args ) { $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { ossengine_log( 'UmamiEngine API request failed, retrying once.' ); $response = wp_remote_request( $url, $args ); } return $response; } /** * Build request headers. * * @param string $token Optional site token. * @return array */ private function headers( $token ) { $headers = array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-Plugin-Version' => OSSENGINE_VERSION, ); if ( '' !== (string) $token ) { $headers['Authorization'] = 'Bearer ' . $token; } return $headers; } /** * Absolute API URL. * * @param string $path API path. * @return string */ private function url( $path ) { return $this->options->backend_url() . $path; } /** * Map a transport response to data or an admin-safe WP_Error. * * @param array|WP_Error $response Transport response. * @return array|WP_Error */ private function parse( $response ) { if ( is_wp_error( $response ) ) { ossengine_log( 'UmamiEngine API transport failure.' ); return new WP_Error( 'ossengine_unreachable', __( 'Unable to connect to UmamiEngine. Please try again.', 'ossengine-analytics-connector' ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( 401 === $code ) { return new WP_Error( 'ossengine_expired', __( 'Your analytics connection has expired. Reconnect to continue tracking.', 'ossengine-analytics-connector' ) ); } if ( 404 === $code ) { return new WP_Error( 'ossengine_not_found', __( 'Your analytics connection was not found. Reconnect to continue tracking.', 'ossengine-analytics-connector' ) ); } if ( 429 === $code ) { return new WP_Error( 'ossengine_rate_limited', __( 'Too many requests. Please wait a minute and try again.', 'ossengine-analytics-connector' ) ); } if ( $code >= 400 && $code < 500 ) { $message = is_array( $data ) && isset( $data['error'] ) ? $data['error'] : __( 'The request was rejected. Please check your settings and try again.', 'ossengine-analytics-connector' ); // Backend 400s may carry the plugin-update notice; pass through, it is admin-safe by contract. return new WP_Error( 'ossengine_rejected', (string) $message ); } if ( $code >= 500 || ! is_array( $data ) ) { ossengine_log( 'UmamiEngine API server failure.' ); return new WP_Error( 'ossengine_unavailable', __( 'Analytics is temporarily unavailable. Your website is unaffected.', 'ossengine-analytics-connector' ) ); } return $data; } }