fix refresh token
This commit is contained in:
@@ -28,6 +28,13 @@ class MauticConnect extends Module
|
|||||||
const MAUTIC_ACCESS_TOKEN = 'MAUTICCONNECT_ACCESS_TOKEN';
|
const MAUTIC_ACCESS_TOKEN = 'MAUTICCONNECT_ACCESS_TOKEN';
|
||||||
const MAUTIC_REFRESH_TOKEN = 'MAUTICCONNECT_REFRESH_TOKEN';
|
const MAUTIC_REFRESH_TOKEN = 'MAUTICCONNECT_REFRESH_TOKEN';
|
||||||
const MAUTIC_TOKEN_EXPIRES = 'MAUTICCONNECT_TOKEN_EXPIRES';
|
const MAUTIC_TOKEN_EXPIRES = 'MAUTICCONNECT_TOKEN_EXPIRES';
|
||||||
|
/**
|
||||||
|
* A static property to cache the access token for the duration of a single request.
|
||||||
|
* This prevents multiple API calls to verify/refresh the token within one script execution.
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
private static $runtimeCache = null;
|
||||||
|
|
||||||
|
|
||||||
// --- DATA-DRIVEN EVENT DEFINITIONS ---
|
// --- DATA-DRIVEN EVENT DEFINITIONS ---
|
||||||
// To add a new event, simply add a new entry to this array.
|
// To add a new event, simply add a new entry to this array.
|
||||||
@@ -384,7 +391,7 @@ class MauticConnect extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ... displayConnectionStatus, getMauticAuthUrl, getOauth2RedirectUri ...
|
// ... displayConnectionStatus, getMauticAuthUrl, getOauth2RedirectUri ...
|
||||||
// ... makeApiRequest, refreshTokenIfNeeded ...
|
|
||||||
// ... hookActionCustomerAccountAdd, hookActionObjectCustomerUpdateAfter, syncCustomer ...
|
// ... hookActionCustomerAccountAdd, hookActionObjectCustomerUpdateAfter, syncCustomer ...
|
||||||
// ... sendOrderEmail, findContactByEmail, and all other helpers remain mostly unchanged ...
|
// ... sendOrderEmail, findContactByEmail, and all other helpers remain mostly unchanged ...
|
||||||
|
|
||||||
@@ -543,6 +550,128 @@ class MauticConnect extends Module
|
|||||||
|
|
||||||
return $this->context->link->getModuleLink($this->name, 'oauth2', [], true);
|
return $this->context->link->getModuleLink($this->name, 'oauth2', [], true);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets a guaranteed valid Mautic access token.
|
||||||
|
*
|
||||||
|
* This function uses a three-level approach for maximum reliability and efficiency:
|
||||||
|
* 1. Runtime Cache: Returns a token if one was already validated in the current script execution.
|
||||||
|
* 2. Database Cache + Live Ping: Uses the stored token but verifies it with a quick API call.
|
||||||
|
* 3. Token Refresh: If the token is expired or invalid, uses the refresh token to get a new one.
|
||||||
|
*
|
||||||
|
* This is the ONLY function you should call to get a token before making an API request.
|
||||||
|
*
|
||||||
|
* @return string The valid access token.
|
||||||
|
* @throws Exception if a valid token cannot be obtained.
|
||||||
|
*/
|
||||||
|
public function getValidAccessToken(): string
|
||||||
|
{
|
||||||
|
// Level 1: Check the runtime cache for this script execution.
|
||||||
|
if (self::$runtimeCache && time() < self::$runtimeCache['expires']) {
|
||||||
|
return self::$runtimeCache['token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
||||||
|
if (!$mauticUrl || !Configuration::get(self::MAUTIC_CLIENT_ID)) {
|
||||||
|
throw new DomainException('Mautic is not configured. Please check the module settings.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessToken = Configuration::get(self::MAUTIC_ACCESS_TOKEN);
|
||||||
|
$expiresAt = (int)Configuration::get(self::MAUTIC_TOKEN_EXPIRES);
|
||||||
|
|
||||||
|
// Level 2: Check the database token and perform a live "ping" to verify it.
|
||||||
|
if ($accessToken && time() < $expiresAt) {
|
||||||
|
try {
|
||||||
|
$client = HttpClient::create(['auth_bearer' => $accessToken]);
|
||||||
|
// A simple, low-impact API call to check if the token is still valid.
|
||||||
|
$client->request('GET', $mauticUrl . '/api/contacts?limit=1');
|
||||||
|
|
||||||
|
// If the ping succeeds, the token is valid. Cache it for this runtime and return.
|
||||||
|
self::$runtimeCache = ['token' => $accessToken, 'expires' => $expiresAt];
|
||||||
|
return $accessToken;
|
||||||
|
} catch (ClientExceptionInterface $e) {
|
||||||
|
if ($e->getResponse()->getStatusCode() !== 401) {
|
||||||
|
// It's a different error (e.g., 404, 500). Log and fail fast.
|
||||||
|
PrestaShopLogger::addLog(
|
||||||
|
sprintf(
|
||||||
|
'Mautic API ping failed with status %d. Response: %s',
|
||||||
|
$e->getResponse()->getStatusCode(),
|
||||||
|
$e->getResponse()->getContent(false)
|
||||||
|
),
|
||||||
|
3 // Severity 3: Error
|
||||||
|
);
|
||||||
|
throw new Exception('Mautic API is not responding correctly. Please check Mautic status.', 0, $e);
|
||||||
|
}
|
||||||
|
// If it IS a 401, the token is dead. Log it and proceed to the refresh logic below.
|
||||||
|
PrestaShopLogger::addLog('Mautic token from database was rejected (401). Forcing refresh.', 1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
PrestaShopLogger::addLog('Mautic API ping failed with a transport error: ' . $e->getMessage(), 3);
|
||||||
|
throw new Exception('Could not connect to Mautic to verify the token.', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 3: The token is expired or was proven invalid. We MUST refresh.
|
||||||
|
PrestaShopLogger::addLog('Attempting to refresh Mautic token.', 1);
|
||||||
|
$refreshToken = Configuration::get(self::MAUTIC_REFRESH_TOKEN);
|
||||||
|
|
||||||
|
if (!$refreshToken) {
|
||||||
|
PrestaShopLogger::addLog('Cannot refresh Mautic token: Refresh Token is missing. Please re-authenticate the module.', 4);
|
||||||
|
throw new DomainException('Mautic Refresh Token is missing. Please reconnect the module.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = HttpClient::create();
|
||||||
|
$response = $client->request('POST', $mauticUrl . '/oauth/v2/token', [
|
||||||
|
'body' => [
|
||||||
|
'client_id' => Configuration::get(self::MAUTIC_CLIENT_ID),
|
||||||
|
'client_secret' => Configuration::get(self::MAUTIC_CLIENT_SECRET),
|
||||||
|
'grant_type' => 'refresh_token',
|
||||||
|
'refresh_token' => $refreshToken,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->toArray(); // Throws on non-2xx responses
|
||||||
|
|
||||||
|
if (empty($data['access_token'])) {
|
||||||
|
PrestaShopLogger::addLog('Mautic refresh response was successful but did not contain an access_token. Response: ' . json_encode($data), 4);
|
||||||
|
throw new Exception('Invalid Mautic refresh response.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// SUCCESS! Save everything to the database AND the runtime cache.
|
||||||
|
$newAccessToken = $data['access_token'];
|
||||||
|
$newRefreshToken = $data['refresh_token'];
|
||||||
|
$newExpiresAt = time() + (int)$data['expires_in'] - 120; // 120s safety buffer
|
||||||
|
|
||||||
|
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, $newAccessToken);
|
||||||
|
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, $newRefreshToken);
|
||||||
|
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, $newExpiresAt);
|
||||||
|
|
||||||
|
self::$runtimeCache = ['token' => $newAccessToken, 'expires' => $newExpiresAt];
|
||||||
|
|
||||||
|
PrestaShopLogger::addLog('Successfully refreshed Mautic access token.', 1);
|
||||||
|
return $newAccessToken;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errorContext = 'CRITICAL: Mautic token refresh failed: ' . $e->getMessage();
|
||||||
|
if ($e instanceof ClientExceptionInterface) {
|
||||||
|
$errorContext .= ' | Mautic Response: ' . $e->getResponse()->getContent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrestaShopLogger::addLog($errorContext, 4); // Severity 4: CRITICAL
|
||||||
|
|
||||||
|
// Invalidate the expired access token. DO NOT touch the refresh token unless Mautic
|
||||||
|
// explicitly tells us it's invalid.
|
||||||
|
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, '');
|
||||||
|
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0);
|
||||||
|
|
||||||
|
if ($e instanceof ClientExceptionInterface && str_contains($e->getResponse()->getContent(false), 'invalid_grant')) {
|
||||||
|
// This is the only case where the refresh token is truly dead.
|
||||||
|
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, '');
|
||||||
|
throw new Exception('Mautic refresh token is invalid or expired. Please reconnect the module in the configuration page.', 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other errors (network issues, Mautic down), we keep the refresh token to try again later.
|
||||||
|
throw new Exception('Could not refresh Mautic token. The system will try again on the next run.', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Core function to make any request to the Mautic API using Symfony HTTP Client.
|
/* Core function to make any request to the Mautic API using Symfony HTTP Client.
|
||||||
* It handles token refreshing, authentication headers, and error checking.
|
* It handles token refreshing, authentication headers, and error checking.
|
||||||
@@ -555,9 +684,18 @@ class MauticConnect extends Module
|
|||||||
*/
|
*/
|
||||||
private function makeApiRequest($endpoint, $method = 'GET', $data = [])
|
private function makeApiRequest($endpoint, $method = 'GET', $data = [])
|
||||||
{
|
{
|
||||||
$this->refreshTokenIfNeeded();
|
try {
|
||||||
|
$accessToken = $this->getValidAccessToken();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// The getValidAccessToken function already logged the critical error.
|
||||||
|
// We just log the consequence and stop this specific operation.
|
||||||
|
PrestaShopLogger::addLog(
|
||||||
|
'valid Mautic token could not be obtained. Error: ' . $e->getMessage(),
|
||||||
|
3 // Severity 3: Error
|
||||||
|
);
|
||||||
|
return false; // Or handle the failure as needed
|
||||||
|
}
|
||||||
|
|
||||||
$accessToken = Configuration::get(self::MAUTIC_ACCESS_TOKEN);
|
|
||||||
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
||||||
|
|
||||||
/** @var \Symfony\Contracts\HttpClient\HttpClientInterface $client */
|
/** @var \Symfony\Contracts\HttpClient\HttpClientInterface $client */
|
||||||
@@ -599,27 +737,97 @@ class MauticConnect extends Module
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the access token is expired and uses the refresh token to get a new one.
|
* Verifies the Mautic API connection by making a lightweight API call.
|
||||||
|
* If the call fails due to an expired token (401), it triggers a token refresh.
|
||||||
|
* This function should be called before making any critical API calls to Mautic.
|
||||||
*
|
*
|
||||||
* @throws Exception if refreshing the token fails.
|
* @throws Exception if the connection is invalid and cannot be refreshed.
|
||||||
*/
|
*/
|
||||||
private function refreshTokenIfNeeded()
|
public function verifyConnectionAndRefresh(): void
|
||||||
|
{
|
||||||
|
$accessToken = Configuration::get(self::MAUTIC_ACCESS_TOKEN);
|
||||||
|
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
||||||
|
|
||||||
|
if (!$accessToken || !$mauticUrl) {
|
||||||
|
// Not configured yet, nothing to do.
|
||||||
|
PrestaShopLogger::addLog('Mautic connection check skipped: module is not configured.', 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check the local expiry time. If it's not expired, do a quick "ping".
|
||||||
|
$expiresAt = (int)Configuration::get(self::MAUTIC_TOKEN_EXPIRES);
|
||||||
|
if (time() < $expiresAt) {
|
||||||
|
try {
|
||||||
|
$client = HttpClient::create([
|
||||||
|
'auth_bearer' => $accessToken,
|
||||||
|
]);
|
||||||
|
// A simple, low-impact API call to check if the token is still valid.
|
||||||
|
$client->request('GET', $mauticUrl . '/api/contacts?limit=1');
|
||||||
|
|
||||||
|
// If the above line doesn't throw, the token is valid. We are done.
|
||||||
|
return;
|
||||||
|
} catch (ClientExceptionInterface $e) {
|
||||||
|
// Check if it's an authentication error (token expired/revoked)
|
||||||
|
if ($e->getResponse()->getStatusCode() === 401) {
|
||||||
|
PrestaShopLogger::addLog('Mautic access token is invalid or expired. Attempting to refresh.', 1); // Severity 1: Informational
|
||||||
|
// The token is invalid, proceed to refresh it below.
|
||||||
|
} else {
|
||||||
|
// Another client error (e.g., 404 Not Found, 403 Forbidden). Log and re-throw.
|
||||||
|
PrestaShopLogger::addLog(
|
||||||
|
sprintf(
|
||||||
|
'Mautic API ping failed with status code %d. Error: %s',
|
||||||
|
$e->getResponse()->getStatusCode(),
|
||||||
|
$e->getResponse()->getContent(false)
|
||||||
|
),
|
||||||
|
3 // Severity 3: Error
|
||||||
|
);
|
||||||
|
throw new Exception('Mautic API ping failed. Please check module configuration and Mautic API status.', 0, $e);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Other errors like network issues.
|
||||||
|
PrestaShopLogger::addLog('Mautic API ping failed with a general error: ' . $e->getMessage(), 3);
|
||||||
|
throw new Exception('Could not connect to Mautic for ping test.', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the code reaches here, it's either because the token is expired locally
|
||||||
|
// or the ping returned a 401. Time to refresh.
|
||||||
|
try {
|
||||||
|
$this->refreshToken(true); // Force refresh
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// The refresh itself failed. The original exception from refreshToken is already logged.
|
||||||
|
// We re-throw it to stop the current process.
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the access token is expired and uses the refresh token to get a new one.
|
||||||
|
* This version is made more resilient and provides detailed logging.
|
||||||
|
*
|
||||||
|
* @param bool $force Forces a refresh attempt even if the token is not expired locally.
|
||||||
|
* @return string The new expiry date.
|
||||||
|
* @throws Exception if refreshing the token fails critically.
|
||||||
|
*/
|
||||||
|
public function refreshToken(bool $force = false): string
|
||||||
{
|
{
|
||||||
$expiresAt = (int)Configuration::get(self::MAUTIC_TOKEN_EXPIRES);
|
$expiresAt = (int)Configuration::get(self::MAUTIC_TOKEN_EXPIRES);
|
||||||
|
|
||||||
if (time() < $expiresAt) {
|
if ((time() + 3600) < $expiresAt && !$force) {
|
||||||
return; // Token is still valid
|
return date("Y-m-d H:i:s", $expiresAt); // Token is still valid
|
||||||
}
|
}
|
||||||
|
|
||||||
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
$mauticUrl = Configuration::get(self::MAUTIC_URL);
|
||||||
$refreshToken = Configuration::get(self::MAUTIC_REFRESH_TOKEN);
|
$refreshToken = Configuration::get(self::MAUTIC_REFRESH_TOKEN);
|
||||||
|
|
||||||
if (!$refreshToken) {
|
if (!$mauticUrl || !$refreshToken) {
|
||||||
throw new Exception('Cannot refresh token: Refresh token is missing.');
|
// Log this critical state
|
||||||
|
PrestaShopLogger::addLog('Cannot refresh Mautic token: Mautic URL or Refresh Token is missing. Please re-authenticate.', 4); // Severity 4: Critical
|
||||||
|
throw new DomainException('Cannot refresh token: Mautic URL or Refresh token is missing in configuration.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = HttpClient::create();
|
$client = HttpClient::create();
|
||||||
|
|
||||||
$postData = [
|
$postData = [
|
||||||
'client_id' => Configuration::get(self::MAUTIC_CLIENT_ID),
|
'client_id' => Configuration::get(self::MAUTIC_CLIENT_ID),
|
||||||
'client_secret' => Configuration::get(self::MAUTIC_CLIENT_SECRET),
|
'client_secret' => Configuration::get(self::MAUTIC_CLIENT_SECRET),
|
||||||
@@ -629,27 +837,50 @@ class MauticConnect extends Module
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $client->request('POST', $mauticUrl . '/oauth/v2/token', [
|
$response = $client->request('POST', $mauticUrl . '/oauth/v2/token', [
|
||||||
// Symfony client correctly encodes this as application/x-www-form-urlencoded
|
|
||||||
'body' => $postData,
|
'body' => $postData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$data = $response->toArray();
|
$data = $response->toArray(); // Throws exception on non-2xx response
|
||||||
|
|
||||||
if (!isset($data['access_token'])) {
|
if (!isset($data['access_token'])) {
|
||||||
|
PrestaShopLogger::addLog('Mautic refresh response did not contain an access_token. Response: ' . json_encode($data), 4);
|
||||||
throw new Exception('Mautic response did not contain an access_token.');
|
throw new Exception('Mautic response did not contain an access_token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success! Save the new tokens and expiry time.
|
// Success! Save the new tokens and expiry time.
|
||||||
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, $data['access_token']);
|
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, $data['access_token']);
|
||||||
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, $data['refresh_token']);
|
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, $data['refresh_token']);
|
||||||
$expiresAt = time() + (int)$data['expires_in'] - 60; // 60s buffer
|
$newExpiresAt = time() + (int)$data['expires_in'] - 120; // 120s buffer for safety
|
||||||
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, $expiresAt);
|
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, $newExpiresAt);
|
||||||
|
|
||||||
|
PrestaShopLogger::addLog('Successfully refreshed Mautic access token. New expiry: ' . date("Y-m-d H:i:s", $newExpiresAt), 1);
|
||||||
|
return date("Y-m-d H:i:s", $newExpiresAt);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
// Critical failure: we can no longer authenticate.
|
// This is the CRITICAL change. We log with high severity but DO NOT delete the refresh token.
|
||||||
|
|
||||||
|
$errorContext = 'Mautic token refresh failed: ' . $e->getMessage();
|
||||||
|
// Try to get the specific API error message from Mautic for better debugging
|
||||||
|
if ($e instanceof ClientExceptionInterface) {
|
||||||
|
$errorContext .= ' | Mautic Response: ' . $e->getResponse()->getContent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrestaShopLogger::addLog($errorContext, 4); // Severity 4: CRITICAL ERROR
|
||||||
|
|
||||||
|
// Invalidate the CURRENT access token, as it's clearly not working.
|
||||||
|
// DO NOT delete the refresh token. It's our only chance to recover.
|
||||||
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, '');
|
Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, '');
|
||||||
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, '');
|
|
||||||
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0);
|
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0);
|
||||||
throw new Exception('Failed to refresh Mautic token. Please reconnect the module. Error: ' . $e->getMessage(), 0, $e);
|
|
||||||
|
// If the error response indicates the refresh token itself is invalid, then we can't recover.
|
||||||
|
if ($e instanceof ClientExceptionInterface && str_contains($e->getResponse()->getContent(false), 'invalid_grant')) {
|
||||||
|
// Now it's truly a fatal error. The refresh token is dead.
|
||||||
|
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, '');
|
||||||
|
throw new Exception('Failed to refresh Mautic token because the refresh token is invalid or expired. Please reconnect the module in the configuration page. Original Error: ' . $e->getMessage(), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors (network, Mautic down), we throw an exception to halt the current operation,
|
||||||
|
// but we preserve the refresh token so the system can try again later.
|
||||||
|
throw new Exception('Failed to refresh Mautic token. The system will try again later. Error: ' . $e->getMessage(), 0, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user