build_url( $path ), array( 'timeout' => RANKTIM_API_TIMEOUT, 'headers' => $this->auth_headers( $key ), ) ); return $this->parse( $response, $path ); } /** * Authenticated POST request. * * @param string $path Path, e.g. "/api/integrations/ack/xxxxx". * @param string $key Plaintext integration key. * @param array $body JSON-encodable body (default: empty object). * @return array|WP_Error */ public function post( string $path, string $key, array $body = array() ) { $response = wp_remote_post( $this->build_url( $path ), array( 'timeout' => RANKTIM_API_TIMEOUT, 'headers' => array_merge( $this->auth_headers( $key ), array( 'Content-Type' => 'application/json' ) ), 'body' => wp_json_encode( $body ), ) ); return $this->parse( $response, $path ); } // ----------------------------------------------------------------------- // Private helpers // ----------------------------------------------------------------------- /** Build absolute URL from a path. */ private function build_url( string $path ): string { return trailingslashit( RANKTIM_BASE_URL ) . ltrim( $path, '/' ); } /** Common request headers (no key logged here). */ private function auth_headers( string $key ): array { return array( 'Authorization' => 'Bearer ' . $key, 'Accept' => 'application/json', ); } /** * Parse a wp_remote_* response. * * Maps transport errors and HTTP status codes to WP_Error or decoded data. * * @param array|WP_Error $response * @param string $path Used only in debug log entries (key-free). * @return array|WP_Error */ private function parse( $response, string $path ) { // Transport / network error. if ( is_wp_error( $response ) ) { Ranktim_Logger::log( '[Ranktim] Network error on ' . $path . ': ' . $response->get_error_message() ); return new WP_Error( 'network_error', __( 'Could not reach ranktim.com. Please try again.', 'ranktim-article-sync' ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); // Auth errors. if ( 401 === $code || 403 === $code ) { Ranktim_Logger::log( '[Ranktim] Auth error HTTP ' . $code . ' on ' . $path ); return new WP_Error( 'auth_error', __( 'Invalid or revoked Integration Key.', 'ranktim-article-sync' ) ); } // Plan / quota error. if ( 402 === $code ) { Ranktim_Logger::log( '[Ranktim] Plan error HTTP 402 on ' . $path ); return new WP_Error( 'plan_error', __( 'Your plan does not allow AI content integrations.', 'ranktim-article-sync' ) ); } // Not found. if ( 404 === $code ) { Ranktim_Logger::log( '[Ranktim] HTTP 404 on ' . $path ); return new WP_Error( 'not_found', __( 'Resource not found.', 'ranktim-article-sync' ) ); } // Rate limited. if ( 429 === $code ) { Ranktim_Logger::log( '[Ranktim] Rate limited (HTTP 429) on ' . $path ); return new WP_Error( 'rate_limited', __( 'Rate limited by Ranktim API. Please wait a few minutes before Sync Now. It will retry automatically on the next scheduled run.', 'ranktim-article-sync' ) ); } // Any other non-2xx. if ( $code < 200 || $code >= 300 ) { Ranktim_Logger::log( '[Ranktim] Unexpected HTTP ' . $code . ' on ' . $path ); return new WP_Error( 'api_error', /* translators: %d = HTTP status code */ sprintf( __( 'API returned status %d.', 'ranktim-article-sync' ), $code ) ); } // Decode JSON. $data = json_decode( $body, true ); if ( null === $data && '' !== trim( $body ) ) { Ranktim_Logger::log( '[Ranktim] Invalid JSON response on ' . $path ); return new WP_Error( 'json_error', __( 'Invalid JSON response from Ranktim API.', 'ranktim-article-sync' ) ); } return is_array( $data ) ? $data : array(); } }