Jake Vanderwerf
2026-01-01 0e4b986e81f8132a44e61fa8df18860301cc3468
inc/integrations/Integrations.php
@@ -62,7 +62,7 @@
    */
   protected array $credentials = [];      // Service credentials (API keys, tokens, etc.)
   protected ?int $userID = null;          // User context for user-specific integrations
   private bool $token_refresh_attempted = false;  // Circuit breaker for token refresh
   protected array $fields = [];       // The fields to generate that become credentials
   protected array $advanced = [];  // The fields that are optional settings
@@ -258,10 +258,6 @@
      if (!empty($this->credentials['expires_at'])) {
         $expires_at = intval($this->credentials['expires_at']);
         if ($expires_at <= time()) {
            // Token expired, try to refresh
            if (!empty($this->credentials['refresh_token'])) {
               return $this->refreshOAuthToken();
            }
            return false;
         }
      }
@@ -765,6 +761,12 @@
      array  $options = []
   ): array|WP_Error
   {
      if (!$this->is_healthy) {
         $this->logDebug('Skipping request - integration is unhealthy', [
            'consecutive_errors' => $this->error_stats['consecutive_errors'],
            'last_success' => $this->error_stats['last_success']
         ]);
      }
      $this->ensureInitialized();
      if (!$this->isSetUp()){
         $this->logError('Connection not setup for '.$this->service_name, [
@@ -778,9 +780,6 @@
         return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.');
      }
      // Debug: Check if credentials are loaded
      error_log('['.$this->service_name.'] Make Request - Credentials loaded: ' . (!empty($this->credentials) ? 'Yes' : 'No'));
      error_log('With Credentials: '.print_r($this->credentials, true));
      $attempt = 0;
      $lastError = null;
@@ -924,8 +923,11 @@
      bool   $force = false
   ): ?array
   {
      $cacheKey = $this->buildCacheKey('GET', $endpoint, $params);
      $ttl = $this->cacheStrategy[$cacheStrategy] ?? $this->ttl;
      $cacheKey = $this->buildCacheKey('GET', $endpoint, $params, $baseKey);
      $ttl = is_int($cacheStrategy)
         ? max(0, $cacheStrategy)
         : ($this->cacheStrategy[$cacheStrategy] ?? $this->ttl);
      if (!$force && $ttl > 0) {
         $cached = $this->cache->get($cacheKey);
@@ -944,7 +946,6 @@
      return $result;
   }
   /**
    * Check if response contains an error
    * Override in child classes for service-specific error detection
@@ -1217,14 +1218,10 @@
   public function handleAjaxResponse()
   {
      error_log('Ajax Response: '.print_r($_GET, true));
      $code = $_GET['code'];
      $state =  $_GET['state'];
      error_log('OAuth Callback - Code: ' . $code);
      error_log('OAuth Callback - State: ' . $state);
      $state_parts = explode('|', $state);
      $state_key = $state_parts[0] ?? '';
@@ -1232,16 +1229,13 @@
      $user_id = ($user_id === 0) ? null : $user_id;
      $return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations');
      error_log('Service: '.print_r($this->service_name, true));
      $state_data = get_transient('oauth_state_' . $state_key);
      error_log('State Data: '.print_r($state_data, true));
      if (!$state_data || $state_data['service'] !== $this->service_name) {
         wp_die('Invalid state parameter', 'OAuth Error');
      }
      // Delete the transient to prevent reuse
      delete_transient('oauth_state_' . $state_key);
      error_log('Return URL: '.print_r($return_url, true));
      // Handle error from OAuth provider
      if (array_key_exists('error', $_GET)) {
         $error_description = $_GET['error_description'] ?? 'Authorization denied';
@@ -1393,17 +1387,29 @@
         if ($this->isOAuthService && $this->hasOAuthCredentials()) {
            // Check if token is expired first
            if (!$this->isOAuthValid()) {
               $this->logDebug('OAuth token expired, attempting refresh');
               if (!$this->refreshOAuthToken()) {
                  $this->logError('Failed to refresh expired OAuth token');
               // Only attempt refresh once per request
               if (!$this->token_refresh_attempted) {
                  $this->token_refresh_attempted = true;
                  $this->logDebug('OAuth token expired, attempting refresh');
                  if (!$this->refreshOAuthToken()) {
                     $this->logError('Failed to refresh expired OAuth token - stopping execution');
                     // Token refresh failed - DO NOT continue making API requests
                     return;
                  }
               } else {
                  // Already attempted refresh in this request
                  $this->logDebug('Token refresh already attempted, skipping');
                  return;
               }
            }
            // Check if we should proactively refresh (before expiry)
            elseif ($this->shouldRefreshToken()) {
            elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) {
               $this->token_refresh_attempted = true;
               $this->logDebug('OAuth token should be refreshed proactively');
               if (!$this->refreshOAuthToken()) {
                  $this->logError('Failed to proactively refresh OAuth token');
                  // Not critical - token is still valid
                  // Not critical - token is still valid, so continue
               }
            }
         }
@@ -1427,6 +1433,7 @@
      // Switch context
      $this->userID = $user_id;
      $this->credentials = [];
      $this->resetTokenRefreshFlag();  // ADD THIS LINE
      $this->ensureInitialized();
   }
@@ -1626,8 +1633,6 @@
      $auth_url = $this->oauth['authorize'] . '?' . http_build_query($params);
      // Debug log for troubleshooting
      error_log("Generated OAuth URL for {$this->service_name}: " . $auth_url);
      return $auth_url;
   }
@@ -1921,7 +1926,6 @@
         return false;
      }
      // Build refresh request data
      $request_data = [
         'client_id' => $this->credentials['client_id'],
         'client_secret' => $this->credentials['client_secret'],
@@ -1929,12 +1933,24 @@
         'grant_type' => 'refresh_token'
      ];
      // Use centralized OAuth request method
      $response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
      if (is_wp_error($response)) {
         $this->logError('Failed to refresh Square token', [
            'error' => $response->get_error_message()
         $error_message = $response->get_error_message();
         if (str_contains($error_message, 'invalid_grant')) {
            $this->logError('OAuth refresh token is invalid - user must re-authorize', [
               'error' => $error_message
            ], 'critical');
            // Mark unhealthy immediately
            $this->error_stats['consecutive_errors'] = $this->error_threshold;
            $this->is_healthy = false;
            $this->saveErrorStats();
         }
         $this->logError('Failed to refresh OAuth token for '.$this->service_name, [
            'error' => $error_message
         ]);
         return false;
      }
@@ -1943,7 +1959,7 @@
         $this->credentials['access_token'] = $response['access_token'];
         $this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days
         // Note: Square returns the SAME refresh token
         // Note: Some services return the SAME refresh token
         if (isset($response['refresh_token'])) {
            $this->credentials['refresh_token'] = $response['refresh_token'];
         }
@@ -3527,4 +3543,12 @@
         throw new Exception('Failed to save JPEG image');
      }
   }
   /**
    * Reset token refresh attempt flag
    * Called automatically when switching users
    */
   protected function resetTokenRefreshFlag(): void
   {
      $this->token_refresh_attempted = false;
   }
}