From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter

---
 inc/integrations/Integrations.php |   66 +++++++++++++++++++++++++++-----
 1 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 5f47ff6..e3ef859 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -167,7 +167,7 @@
 	{
 		$this->cacheName = $this->cacheName ?: $this->service_name;
 		$this->userID = $userID;
-		$this->cache = new CacheManager('integrations_' . $this->cacheName, $this->ttl);
+		$this->cache = CacheManager::for('integrations_' . $this->cacheName, $this->ttl);
 
 		// Load error stats from cache
 		$this->loadErrorStats();
@@ -846,14 +846,14 @@
 
 		$this->logDebug("$method request to: $url: ".print_r($args, true));
 
-		// Standard WordPress HTTP API
-		// Use appropriate WordPress HTTP function
+		// Make the request
 		$response = match($method) {
 			'GET' => wp_remote_get($url, $args),
 			'POST' => wp_remote_post($url, $args),
 			'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
 			default => null
 		};
+
 		if (!$response) {
 			$this->logError("Unsupported HTTP method $method for $this->service_name");
 			return null;
@@ -867,9 +867,42 @@
 		$response_code = wp_remote_retrieve_response_code($response);
 		$body = wp_remote_retrieve_body($response);
 
+		// Handle 401 - try to refresh token and retry once
+		if ($response_code === 401 && $this->isOAuthService && !empty($this->credentials['refresh_token'])) {
+			// Avoid infinite retry loop - only retry once
+			static $retry_count = 0;
+
+			if ($retry_count === 0) {
+				$retry_count++;
+
+				$this->logDebug('Got 401, attempting token refresh...');
+				if ($this->refreshOAuthToken()) {
+					$this->logDebug('Token refreshed successfully, retrying request...');
+
+					// Rebuild request args with new token
+					$args = $this->buildRequestArgs($method, $data, $options);
+
+					// Retry the request
+					$response = match($method) {
+						'GET' => wp_remote_get($url, $args),
+						'POST' => wp_remote_post($url, $args),
+						'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
+						default => null
+					};
+
+					if ($response && !is_wp_error($response)) {
+						$response_code = wp_remote_retrieve_response_code($response);
+						$body = wp_remote_retrieve_body($response);
+					}
+				}
+				$retry_count = 0; // Reset for next request
+			}
+		}
+
 		if ($response_code >= 400) {
 			$this->handleApiError($response_code, $body, $endpoint);
 		}
+
 		$decoded = json_decode($body, true);
 		if (json_last_error() !== JSON_ERROR_NONE) {
 			return ['raw_response' => $body];
@@ -904,7 +937,8 @@
 
 		$result = $this->makeRequest('GET', $endpoint, $params, $baseKey);
 
-		if ($result && $ttl > 0) {
+		// Only cache successful responses (not WP_Error and not error objects)
+		if ($result && !is_wp_error($result) && !$this->isErrorResponse($result) && $ttl > 0) {
 			$this->cache->set($cacheKey, $result, $ttl);
 		}
 
@@ -912,6 +946,18 @@
 	}
 
 	/**
+	 * Check if response contains an error
+	 * Override in child classes for service-specific error detection
+	 */
+	protected function isErrorResponse(array $response): bool
+	{
+		// Common error patterns across APIs
+		return isset($response['error'])
+			|| isset($response['errors'])
+			|| isset($response['error_description']);
+	}
+
+	/**
 	 * POST request
 	 */
 	protected function postRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
@@ -1758,12 +1804,7 @@
 			'redirect_uri' => $this->getRedirectUri()
 		];
 
-		// Use a custom endpoint key for OAuth (not part of regular API)
-		// We need to handle this specially since OAuth endpoints are different
 		$oauth_endpoint = $this->oauth['token'];
-
-		// Make the request using the centralized method
-		// This automatically includes rate limiting and error handling
 		$response = $this->makeOAuthRequest('POST', $oauth_endpoint, $request_data);
 
 		if (is_wp_error($response)) {
@@ -1776,10 +1817,13 @@
 
 		// Parse response
 		if (isset($response['access_token'])) {
+			$expires_in = $response['expires_in'] ?? 2592000; // 30 days default
+
 			return [
 				'access_token' => $response['access_token'],
 				'refresh_token' => $response['refresh_token'] ?? '',
-				'expires_in' => $response['expires_in'] ?? 2592000, // 30 days default
+				'expires_in' => $expires_in,
+				'expires_at' => time() + $expires_in, // Calculate expiry timestamp
 				'token_type' => $response['token_type'] ?? 'Bearer',
 				'merchant_id' => $response['merchant_id'] ?? '',
 				'scope' => $response['scope'] ?? ''
@@ -3064,7 +3108,7 @@
 		}
 		$credentials = $this->getCredentials();
 		$hasCredentials = $this->hasOAuthCredentials();
-		$returnURL = (is_admin()) ? :get_the_permalink();
+		$returnURL = is_admin() ? admin_url('admin.php?page=jvb-integrations') : (get_the_permalink() ?: home_url());
 		?>
 
 		<details <?= $hasCredentials?' open':''?>>

--
Gitblit v1.10.0