exact-php-client: Webhook and recurring "Could not acquire or refresh tokens"
Not long ago we discussed the issue with “Could not acquire or refresh tokens” under #385 and after I updated my code, it looked like it was solved. but the joy was shortly lived. Right now I am again experiencing this issue but it looks like it’s pooping up randomly. I integrated all said in the #385 and the code works fine for about 95% of the time, it even handles multiple calls from ExactOnline properly (not executing code in parallel, but waiting for other process to finish), but as said, I still see the mentioned issue from time to time. Because of this, I also implemented that if this happens, our server responds with 500 so that ExactOnline’s Webhook re-sends the data later.
This is why I would like to know on how exactly and properly to use Webhook and multiple calls to it. I thought I understand it, but it would look like I am missing something.
Our environment is core PHP (for now, will move to Laravel at some point) and it goes like this:
- a single user is responsible to connect ExactOnline to our app and only one user can do it
- that user can successfully authenticate ExactOnline
- when authenticated, code requests for tokens and also checks if a Webhook Subscription (for Topic “Accounts” only) exists
- if it does not, it creates it (code below)
- then the code is waiting for incoming calls from Webhook, updated this code with suggestions in #385
- once it receives the requests, it checks it out if its authorized and then calls ExactOnline to pull the data and process it on our end
- then the code responds with 200 or 500
If someone has some free time, would really appreciate if he or she can check out my code and let me know where I went wrong. Cache I am using is phpFastCache 7.0 and it is initialized like this:
use \Phpfastcache\Exceptions\PhpfastcacheInvalidConfigurationException;
try {
CacheManager::setDefaultConfig(new Config([
'path' => dir_root . '_cache',
"itemDetailedDate" => false,
'secureFileManipulation ' => true
]));
} catch (PhpfastcacheInvalidConfigurationException $e) {
} catch (ReflectionException $e) {
}
$cache = new Psr16Adapter('Files');
Cache could easily be replaced with flock, but since I am already using it in the project, decided to use it here too.
So, the request comes to /exact-online-webhook, then it includes the webhook.php and calls clients_webhook() (please note that I removed some code that’s debugging related … it merely logs different states into .txt file, does not interfere how the code works):
define('webhook', true);
require_once(dir_root . 'classes/sync.php');
use \Picqer\Financials\Exact\Account;
function clients_webhook()
{
global $exact_credentials;
$input = file_get_contents('php://input');
$authState = authenticate($input, $exact_credentials['webhook_secret']);
$data = json_decode($input, true);
if ($authState) {
try {
$connection = connect();
if ($connection !== false) {
$CRMClients = new Account($connection);
$clients = $CRMClients->filter("ID eq guid'" . $data['Content']['Key'] . "'", '', 'ID,AddressLine1,AddressLine2,City,Country,CountryName,Created,Email,Language,MainContact,Modified,Name,Phone,PhoneExtension,Postcode,Status');
$results = [];
foreach ($clients as $client) {
$data = [
':exact_id' => $client->ID,
':url' => URLify::filter($client->Name),
':company_name' => (!empty($client->Name) ? trim($client->Name) : ''),
':address' => (!empty($client->AddressLine1) ? trim($client->AddressLine1) : '') . (!empty($client->AddressLine2) ? ' ' . trim($client->AddressLine2) : ''),
':postalcode' => (!empty($client->Postcode) ? trim($client->Postcode) : ''),
':city' => (!empty($client->City) ? ucwords(trim($client->City)) : ''),
':country' => (!empty($client->Country) ? trim($client->Country) : ''),
':country_id' => (!empty($client->Country) ? get_country_id(trim($client->Country)) : 0),
':telephone' => (!empty($client->Phone) ? trim($client->Phone) : ''),
':email' => (!empty($client->Email) ? trim($client->Email) : '')
];
if (save_client($data)) {
$results[] = true;
} else {
$results[] = false;
}
}
setValue('last_exact_online_sync', date('Y-m-d\TH:i:s', time()));
} else {
return false;
}
} catch (Exception $e) {
trigger_error('connection for webhook failed: ' . $e->getMessage());
return false;
}
} else {
return false;
}
}
sync.php:
use Phpfastcache\Exceptions\PhpfastcacheSimpleCacheException;
use Picqer\Financials\Exact\Connection;
use Picqer\Financials\Exact\WebhookSubscription;
use Picqer\Financials\Exact\Account;
if (defined('webhook') === true) {
require_once(dir_root . 'vendor/autoload.php');
function getValue($key)
{
global $pdo;
if (in_array($key, [
'authorizationCode',
'webhook_subscribed',
'accessToken',
'refreshToken',
'last_exact_online_sync'
])) {
try {
$db = $pdo->prepare("SELECT
`configuration`.`$key`
FROM `configuration`
WHERE `configuration`.`id` = 1");
$db->execute();
if ($db->rowCount() > 0) {
return $db->fetchColumn();
} else {
return false;
}
} catch (PDOException $e) {
trigger_error("MySQL get value from DB error: " . $e->getMessage());
}
}
return false;
}
function setValue($key, $value)
{
global $pdo;
if (in_array($key, [
'authorizationCode',
'webhook_subscribed',
'accessToken',
'refreshToken',
'expiresIn',
'last_exact_online_sync'
])) {
try {
$p = [':value' => $value];
$db = $pdo->prepare("UPDATE `configuration` SET `configuration`.`$key`=:value WHERE `configuration`.`id` = '1'");
$db->execute($p);
return true;
} catch (PDOException $e) {
trigger_error("MySQL set value into DB error: " . $e->getMessage());
}
}
return false;
}
function authorize($returnUrl = null)
{
global $exact_credentials;
$connection = new Connection();
$connection->setRedirectUrl($exact_credentials['redirect_url']);
$connection->setExactClientId($exact_credentials['client_id']);
$connection->setExactClientSecret($exact_credentials['client_secret']);
return $returnUrl ? $connection->getAuthUrl() : $connection->redirectForAuthorization();
}
function connect()
{
global $exact_credentials, $cache;
$connection = new Connection();
$connection->setRedirectUrl($exact_credentials['redirect_url']);
$connection->setExactClientId($exact_credentials['client_id']);
$connection->setExactClientSecret($exact_credentials['client_secret']);
$authorizationCode = getValue('authorizationCode');
if (!empty($authorizationCode)) {
$connection->setAuthorizationCode($authorizationCode);
}
$accessToken = getValue('accessToken');
if (!empty($accessToken)) {
$connection->setAccessToken($accessToken);
}
$refreshToken = getValue('refreshToken');
if (!empty($refreshToken)) {
$connection->setRefreshToken($refreshToken);
}
$expiresIn = getValue('expiresIn');
if (!empty($expiresIn)) {
$connection->setTokenExpires($expiresIn);
}
$connection->setTokenUpdateCallback(function ($connection) {
setValue('accessToken', $connection->getAccessToken());
setValue('refreshToken', $connection->getRefreshToken());
setValue('expiresIn', $connection->getTokenExpires() - (2 * 60));
});
$connection->setRefreshAccessTokenCallback(function ($connection) {
$connection->setAccessToken(getValue('accessToken'));
$connection->setRefreshToken(getValue('refreshToken'));
$connection->setTokenExpires(getValue('expiresIn'));
});
$connection->setAcquireAccessTokenLockCallback(function () {
return mutex_lock();
});
$connection->setAcquireAccessTokenUnlockCallback(function () {
return mutex_unlock();
});
try {
$connection->connect();
/*
setValue('accessToken', $connection->getAccessToken());
setValue('refreshToken', $connection->getRefreshToken());
setValue('expiresIn', $connection->getTokenExpires() - (2 * 60));
*/
webhookSubscribe($connection);
return $connection;
} catch (Exception $e) {
trigger_error('Could not connect to Exact: ' . $e->getMessage());
return false;
}
}
function check_if_client_exists($id)
{
global $pdo;
try {
$p = [':exact_id' => $id];
$db = $pdo->prepare("SELECT
`clients`.`id`
FROM `clients`
WHERE `clients`.`exact_id`=:exact_id");
$db->execute($p);
if ($db->rowCount() > 0) {
return true;
} else {
return false;
}
} catch (PDOException $e) {
trigger_error("MySQL check if client exists in DB error: " . $e->getMessage());
file_put_contents(ini_get('error_log'), print_r($p, true), FILE_APPEND);
return false;
}
}
function save_client($data)
{
global $pdo;
$client_exists = check_if_client_exists($data[':exact_id']);
try {
$sql = MySQLString($data);
$db = $pdo->prepare("INSERT INTO `clients` ({$sql[0]}) VALUES ({$sql[1]}) ON DUPLICATE KEY UPDATE {$sql[2]}");
$db->execute($data);
return true;
} catch (PDOException $e) {
trigger_error("MySQL save new or update existing client error: " . $e->getMessage());
file_put_contents(ini_get('error_log'), print_r($data, true), FILE_APPEND);
return false;
}
}
function authenticate($requestContent, $webhookSecret)
{
$matches = [];
$matched = preg_match('/^{"Content":(.*),"HashCode":"(.*)"}$/', $requestContent, $matches);
if ($matched === 1 && isset($matches[1]) && isset($matches[2])) {
return $matches[2] === strtoupper(hash_hmac('sha256', $matches[1], $webhookSecret));
}
return false;
}
function mutex_lock()
{
global $cache;
for ($i = 0; $i < 90 && $cache->has('exact-locked'); $i++) {
sleep(1);
}
if (!$cache->set('exact-locked', true, 60)) {
trigger_error('Mutex: unable to get a lock for new request!');
}
return true;
}
function mutex_unlock()
{
global $cache;
return $cache->delete('exact-locked');
}
function webhookSubscribe($connection)
{
if (empty(getValue('webhook_subscribed'))) {
try {
$Webhook = new WebhookSubscription($connection);
$Webhook->CallbackURL = site_domain_full . '/api/v1/exact-online-webhook/v2';
$Webhook->Topic = 'Accounts';
$Webhook->save();
setValue('webhook_subscribed', 1);
} catch (Exception $e) {
trigger_error('Unable to set Webhook Subscription: ' . $e->getMessage());
}
}
}
}
Using PHP v7.4, exact-php-client v3.26.0
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 2
- Comments: 20 (4 by maintainers)
Omg wasted 3 days on this. Thanks to logs of the database. Was making a transaction, which in case of failure would also drop this store request. Thanks, @MaartenWaegeman for proactive help
A thank you from me as well @MaartenWaegeman; have applied the Atomic lock principe to the laravel-exact-online package as well and haven’t run into the 401 error since then
To get a proper refresh from multiple processes you should implement 4 callback functions:
My callback functions look like this (Laravel)