access_token = $api_key; $this->api_key_auth = true; } } /** * Connect using API key * Tests connection by calling /v1/integration/integrations/test-connection/ endpoint * * @param string $api_key API key from IGNY8 app * @param int $site_id Site ID from IGNY8 app * @return bool True on success, false on failure */ public function connect($api_key, $site_id = null) { if (empty($api_key)) { return false; } // Store API key if (function_exists('igny8_store_secure_option')) { igny8_store_secure_option('igny8_api_key', $api_key); } else { update_option('igny8_api_key', $api_key); } $this->access_token = $api_key; $this->api_key_auth = true; // If site_id provided, test connection to integration endpoint if (!empty($site_id)) { $test_response = $this->post('/v1/integration/integrations/test-connection/', array( 'site_id' => (int) $site_id, 'api_key' => $api_key, 'site_url' => get_site_url() )); if ($test_response['success']) { $timestamp = current_time('timestamp'); update_option('igny8_last_api_health_check', $timestamp); return true; } return false; } // Fallback: if no site_id, just verify API key exists by making a simple call // This tests that the API key is valid format at least $timestamp = current_time('timestamp'); update_option('igny8_last_api_health_check', $timestamp); return true; } /** * Check if API is authenticated * * @return bool True if authenticated, false otherwise */ public function is_authenticated() { return !empty($this->access_token); } /** * Get the API base URL * * @return string API base URL */ public function get_api_base() { return $this->base_url; } /** * Parse unified API response * * @param array|WP_Error $response HTTP response * @return array Parsed response */ private function parse_response($response) { if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message(), 'http_status' => 0 ); } $status_code = wp_remote_retrieve_response_code($response); $raw_body = wp_remote_retrieve_body($response); $body = json_decode($raw_body, true); // Handle non-JSON responses — allow empty arrays/objects but detect JSON decode errors if (json_last_error() !== JSON_ERROR_NONE) { return array( 'success' => false, 'error' => 'Invalid JSON response: ' . json_last_error_msg(), 'raw_body' => substr($raw_body, 0, 200), 'http_status' => $status_code ); } // Check if response follows unified format if (isset($body['success'])) { $body['http_status'] = $status_code; // Handle throttling errors (429) - extract retry delay from error message if ($status_code === 429 && isset($body['error'])) { // Extract delay from error message like "Request was throttled. Expected available in 1 second." if (preg_match('/Expected available in (\d+) second/i', $body['error'], $matches)) { $body['retry_after'] = intval($matches[1]); } elseif (preg_match('/(\d+) second/i', $body['error'], $matches)) { $body['retry_after'] = intval($matches[1]); } else { // Default to 2 seconds if we can't parse it $body['retry_after'] = 2; } } return $body; } // Legacy format - wrap in unified format if ($status_code >= 200 && $status_code < 300) { return array( 'success' => true, 'data' => $body, 'http_status' => $status_code ); } else { $error_message = $body['detail'] ?? 'HTTP ' . $status_code . ' error'; // Handle throttling in legacy format $retry_after = null; if ($status_code === 429) { if (preg_match('/Expected available in (\d+) second/i', $error_message, $matches)) { $retry_after = intval($matches[1]); } elseif (preg_match('/(\d+) second/i', $error_message, $matches)) { $retry_after = intval($matches[1]); } else { $retry_after = 2; } } $result = array( 'success' => false, 'error' => $error_message, 'http_status' => $status_code, 'raw_error' => $body ); if ($retry_after !== null) { $result['retry_after'] = $retry_after; } return $result; } } /** * Get headers with authentication * Uses Bearer token format for API key authentication * * @return array Headers array */ private function get_headers() { $headers = array( 'Content-Type' => 'application/json', 'Accept' => 'application/json' ); if (!empty($this->access_token)) { $headers['Authorization'] = 'Bearer ' . $this->access_token; } return $headers; } /** * Make GET request with automatic retry on throttling * * @param string $endpoint API endpoint (e.g. /v1/auth/sites/ or /v1/integration/integrations/) * @param int $max_retries Maximum number of retries for throttled requests (default: 3) * @return array Response data */ public function get($endpoint, $max_retries = 3) { if (!$this->is_authenticated()) { return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); } // Ensure endpoint starts with /v1 if (strpos($endpoint, '/v1/') === false) { if (strpos($endpoint, '/') !== 0) { $endpoint = '/' . $endpoint; } if (strpos($endpoint, '/v1') !== 0) { $endpoint = '/v1' . $endpoint; } } $url = $this->base_url . $endpoint; $headers = $this->get_headers(); $retry_count = 0; while ($retry_count <= $max_retries) { // Debug logging (enable with WP_DEBUG or IGNY8_DEBUG constant) $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); if ($debug_enabled) { error_log(sprintf( 'IGNY8 DEBUG GET: %s | Headers: %s', $url, json_encode(array_merge($headers, array('Authorization' => 'Bearer ***'))) )); } $response = wp_remote_get($url, array( 'headers' => $headers, 'timeout' => 30 )); // Debug response if ($debug_enabled) { $status_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); error_log(sprintf( 'IGNY8 DEBUG RESPONSE: Status=%s | Body=%s', $status_code, substr($response_body, 0, 500) )); } $body = $this->parse_response($response); // If throttled (429), retry after the specified delay if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; // Add a small buffer (0.5 seconds) to ensure we wait long enough $wait_time = $retry_after + 0.5; $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up // Log retry attempt if ($debug_enabled) { error_log(sprintf( 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', $wait_time, $retry_count + 1, $max_retries )); } // Wait before retrying sleep($wait_seconds); $retry_count++; continue; } // Not throttled or max retries reached, return response // API keys don't expire, so no refresh logic needed // If 401, the API key is invalid or revoked return $body; } // Should never reach here, but return last response if we do return $body; } /** * Make POST request with automatic retry on throttling * * @param string $endpoint API endpoint (e.g. /v1/integration/integrations/) * @param array $data Request data * @param int $max_retries Maximum number of retries for throttled requests (default: 3) * @return array Response data */ public function post($endpoint, $data, $max_retries = 3) { // Special case: test-connection endpoint allows API key in request body // So we don't require pre-authentication for this endpoint $is_test_connection = (strpos($endpoint, 'test-connection') !== false); $has_api_key_in_data = !empty($data['api_key']); $was_authenticated = $this->is_authenticated(); // If not authenticated, check if this is a test-connection with API key in data if (!$was_authenticated) { if ($is_test_connection && $has_api_key_in_data) { // Temporarily set the API key for this request $temp_api_key = $this->access_token; $this->access_token = $data['api_key']; } else { return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); } } // Ensure endpoint starts with /v1 if (strpos($endpoint, '/v1/') === false) { if (strpos($endpoint, '/') !== 0) { $endpoint = '/' . $endpoint; } if (strpos($endpoint, '/v1') !== 0) { $endpoint = '/v1' . $endpoint; } } $retry_count = 0; while ($retry_count <= $max_retries) { $response = wp_remote_post($this->base_url . $endpoint, array( 'headers' => $this->get_headers(), 'body' => json_encode($data), 'timeout' => 60 )); $body = $this->parse_response($response); // If throttled (429), retry after the specified delay if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; // Add a small buffer (0.5 seconds) to ensure we wait long enough $wait_time = $retry_after + 0.5; $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up // Log retry attempt $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); if ($debug_enabled) { error_log(sprintf( 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', $wait_time, $retry_count + 1, $max_retries )); } // Wait before retrying sleep($wait_seconds); $retry_count++; continue; } // Not throttled or max retries reached, return response // Restore original access token if we temporarily set it if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) { $this->access_token = isset($temp_api_key) ? $temp_api_key : null; } return $body; } // Should never reach here, but return last response if we do // Restore original access token if we temporarily set it if ($is_test_connection && $has_api_key_in_data && !$was_authenticated) { $this->access_token = isset($temp_api_key) ? $temp_api_key : null; } return $body; } /** * Make PUT request with automatic retry on throttling * * @param string $endpoint API endpoint (e.g. /v1/integration/integrations/1/update-structure/) * @param array $data Request data * @param int $max_retries Maximum number of retries for throttled requests (default: 3) * @return array Response data */ public function put($endpoint, $data, $max_retries = 3) { if (!$this->is_authenticated()) { return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); } // Ensure endpoint starts with /v1 if (strpos($endpoint, '/v1/') === false) { if (strpos($endpoint, '/') !== 0) { $endpoint = '/' . $endpoint; } if (strpos($endpoint, '/v1') !== 0) { $endpoint = '/v1' . $endpoint; } } $retry_count = 0; while ($retry_count <= $max_retries) { $response = wp_remote_request($this->base_url . $endpoint, array( 'method' => 'PUT', 'headers' => $this->get_headers(), 'body' => json_encode($data), 'timeout' => 60 )); $body = $this->parse_response($response); // If throttled (429), retry after the specified delay if (isset($body['http_status']) && $body['http_status'] === 429 && $retry_count < $max_retries) { $retry_after = isset($body['retry_after']) ? $body['retry_after'] : 2; $wait_time = $retry_after + 0.5; $wait_seconds = (int) ceil($wait_time); // Convert to integer, rounding up $debug_enabled = (defined('WP_DEBUG') && WP_DEBUG) || (defined('IGNY8_DEBUG') && IGNY8_DEBUG); if ($debug_enabled) { error_log(sprintf( 'IGNY8 DEBUG: Request throttled, retrying after %.1f seconds (attempt %d/%d)', $wait_time, $retry_count + 1, $max_retries )); } sleep($wait_seconds); $retry_count++; continue; } return $body; } return $body; } /** * Make DELETE request * * @param string $endpoint API endpoint * @return array Response data */ public function delete($endpoint) { if (!$this->is_authenticated()) { return array('success' => false, 'error' => 'Not authenticated', 'http_status' => 401); } $response = wp_remote_request($this->base_url . $endpoint, array( 'method' => 'DELETE', 'headers' => $this->get_headers(), 'timeout' => 30 )); return $this->parse_response($response); } /** * Get access token * * @return string|null Access token */ public function get_access_token() { return $this->access_token; } }