ExactOnline, веб-перехватчики и ошибка "Не удалось получить или обновить токены [http 401]"
Мы используем Picquer-Exact-PHP-Client для доступа к ExactOnline, но мы сталкиваемся со случайной ошибкой "Не удалось получить или обновить токены [http 401]" за последний год (или уже два?) И хотели подтвердить несколько вещей, если они сделаны правильно.
Я разработал веб-приложение, которое использует клиент, и это веб-приложение подключено к веб-перехватчику ExactOnline. Мы (на данный момент) используем это соединение для получения уведомлений от ExactOnline, когда учетная запись была создана, отредактирована или удалена. Это работает в течение некоторого времени (от нескольких дней до нескольких недель), но затем происходит случайный сбой с указанной ошибкой. К сожалению, мне не повезло с отладкой и исправлением этой проблемы, так как я действительно все перепробовал.
Рабочий процесс такой:
- наш клиент запрашивает синхронизацию с ExactOnline и затем перенаправляется на веб-сайт ExactOnline для подтверждения запроса.
- клиент авторизует запрос и затем возвращается в наше веб-приложение
- код работает, и синхронизация выполняется тут же
- создается веб-перехватчик, который прослушивает определенные вызовы ExactOnline (Тема = Учетные записи)
- это работает в течение некоторого времени (от нескольких дней до нескольких недель)
- затем мы получаем случайную ошибку Не удалось получить или обновить токены [http 401] без четкого сообщения об ошибке или исключения
- Мне нужно вручную удалить учетные данные Webhook и ExactOnline
- клиенту требуется повторная авторизация, и мы начинаем с пункта 1.
Я знаю, что проблема возникает из-за истечения срока действия токена доступа, но это должно быть решено модулем, который мы используем. Они утверждают, что по истечении срока действия токена доступа они отправляют токен обновления для получения нового токена. И поскольку токен доступа действителен только в течение 10 минут, это, очевидно, работает правильно. Но теперь я начал задаваться вопросом (и нигде не могу его найти), истекает ли срок действия токена обновления! Поскольку это была бы единственная логическая причина, по которой мы видим эту ошибку. Кто-нибудь может подтвердить, что это возможно?
Код. Это файл sync.php, который обрабатывает соединение, обновление токена, начальную синхронизацию, а также сохраняет данные в базе данных при получении (посредством начальной синхронизации или уведомления веб-перехватчика):
require_once(dir_root . 'vendor/autoload.php');
function getValue($key)
{
global $pdo;
if (in_array($key, array(
'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, array(
'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());
file_put_contents(ini_get('error_log'), print_r($p, true), FILE_APPEND);
}
}
return false;
}
function authorize($returnUrl = null)
{
global $exact_credentials;
$connection = new \Picqer\Financials\Exact\Connection();
$connection->setRedirectUrl($exact_credentials['redirect_url']);
$connection->setExactClientId($exact_credentials['client_id']);
$connection->setExactClientSecret($exact_credentials['client_secret']);
if (empty($returnUrl)) {
$connection->redirectForAuthorization();
} else {
return $connection->redirectForAuthorization(true);
}
}
function tokenUpdateCallback(\Picqer\Financials\Exact\Connection $connection)
{
setValue('accessToken', $connection->getAccessToken());
setValue('refreshToken', $connection->getRefreshToken());
setValue('expiresIn', $connection->getTokenExpires());
return true;
}
function connect()
{
global $exact_credentials;
$connection = new \Picqer\Financials\Exact\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('tokenUpdateCallback');
try {
$connection->connect();
setValue('accessToken', $connection->getAccessToken());
setValue('refreshToken', $connection->getRefreshToken());
setValue('expiresIn', $connection->getTokenExpires());
return $connection;
} catch (\Exception $e) {
trigger_error('Could not connect to Exact: ' . $e->getMessage());
return false;
}
}
function sync_clients()
{
global $webhook_registered;
try {
$connection = connect();
if ($connection === false) {
//setValue('authorizationCode', '');
authorize();
}
} catch (\Exception $e) {
//setValue('authorizationCode', '');
authorize();
die();
}
// check if webhook was subscribed, otherwise subscribe it
if (empty(getValue('webhook_subscribed'))) {
try {
$Webhook = new \Picqer\Financials\Exact\WebhookSubscription($connection);
// $Webhook->deleteSubscriptions($connection);
/*
foreach ($subs as $sub) {
$sub->delete();
}
*/
$Webhook->CallbackURL = site_domain_full . '/api/v1/exact-online-webhook/v2';
$Webhook->Topic = 'Accounts';
$Webhook->save();
$webhook_registered = true;
setValue('webhook_subscribed', 1);
} catch (\Exception $e) {
trigger_error('Unable to set Webhook Subscription: ' . $e->getMessage());
$webhook_registered = false;
}
}
// get last sync date
$last_sync_tmp = getValue('last_exact_online_sync');
$last_sync = '';
if (!empty($last_sync_tmp) && $last_sync_tmp !== '0000-00-00 00:00:00') {
$last_sync = " and Created ge datetime'" . date('Y-m-d\TH:i:s', strtotime($last_sync_tmp)) . "'";
}
// get the customers from Exact Online
try {
$CRMClients = new \Picqer\Financials\Exact\Account($connection);
$clients = $CRMClients->filter("Status eq 'C'$last_sync", '', 'ID,AddressLine1,AddressLine2,City,Country,CountryName,Created,Email,Language,MainContact,Modified,Name,Phone,PhoneExtension,Postcode,Status');
// loop through results and check if record already exist in DB
foreach ($clients as $client) {
// compile data
$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_id' => (!empty($client->Country) ? get_country_id(trim($client->Country)) : 0),
':country' => (!empty($client->Country) ? trim($client->Country) : ''),
':telephone' => (!empty($client->Phone) ? trim($client->Phone) : ''),
':email' => (!empty($client->Email) ? trim($client->Email) : ''),
':created' => (!empty($client->Created) ? date('Y-m-d H:i:s', substr(str_replace(array(
'/Date(',
')/'
), '', $client->Created), 0, -3)) : '')
];
if (!check_if_client_exists($client->ID)) {
// new client found, compile some data to be returned so it's presented to the user
$new_clients[] = $data;
}
// save client into database, even if it does exist from before
save_client($data, true);
/*
echo 'Customer:<br>';
echo '>> '. $client->ID . '<br>';
echo '>> '. $client->Name . '<br>';
echo '>> '. $client->AddressLine1 . '<br>';
echo '>> '. $client->AddressLine2 . '<br>';
echo '>> '. $client->City . '<br>';
echo '>> '. $client->Postcode . '<br>';
echo '>> '. $client->Country . '<br>';
echo '>> '. $client->CountryName . '<br>';
echo '>> '. $client->Created . '<br>';
echo '>> '. $client->Modified . '<br>';
echo '>> '. $client->Status . '<br>';
echo '>> '. $client->Email . '<br>';
echo '>> '. $client->Language . '<br>';
echo '>> '. $client->Phone . '<br>';
echo '>> '. $client->PhoneExtension . '<br>';
*/
}
setValue('last_exact_online_sync', date('Y-m-d\TH:i:s', time()));
if (!empty($new_clients)) {
return $new_clients;
} else {
return true;
}
} catch (Exception $e) {
trigger_error('Exact Online: ' . get_class($e) . ' : ' . $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, $write_log = false)
{
global $pdo;
// get lat/lng
require_once(dir_root . 'classes/class.clients.php');
global $t, $l, $deleted_items;
$clients = new Clients($pdo, $t, $l, $deleted_items);
$address = [
$data[':address'],
$data[':postalcode'],
$data[':city'],
$data[':country']
];
$coords = $clients->geocodeAddress($address);
if (!empty($coords)) {
$data[':lat'] = $coords['lat'];
$data[':lng'] = $coords['lng'];
}
// check URL and create folder
if (empty($data[':url'])) {
$data[':url'] = URLify::filter($data[':company_name']);
}
try {
$p = [
':id' => $data[':exact_id'],
':url' => $data[':url']
];
$db = $pdo->prepare("SELECT `clients`.`url` FROM `clients` WHERE `clients`.`url`=:url AND `clients`.`exact_id`!=:id");
$db->execute($p);
if ($db->rowCount() > 0) {
$go = true;
$i = 1;
$tmpurl = $data[':url'];
while ($go == true) {
$data[':url'] = $tmpurl . '-' . $i;
try {
$p = [
':id' => $data[':exact_id'],
':url' => $data[':url']
];
$db = $pdo->prepare("SELECT `clients`.`url` FROM `clients` WHERE `clients`.`url`=:url AND `clients`.`exact_id`!=:id");
$db->execute($p);
if ($db->rowCount() == 0) {
$go = false;
}
} catch (PDOException $e) {
trigger_error("MySQL check url's uniqueness error: " . $e->getMessage());
}
$i++;
}
}
} catch (PDOException $e) {
trigger_error("MySQL check url's uniqueness error: " . $e->getMessage());
}
// rename folder or create it if missing
try {
$p = [':id' => $data[':exact_id']];
$db = $pdo->prepare("SELECT
`clients`.`url`
FROM `clients`
WHERE `clients`.`exact_id`=:id");
$db->execute($p);
if ($db->rowCount() > 0) {
$d = $db->fetch();
if ($d['url'] != $data[':url']) {
if (file_exists(dir_uploads . 'clients/' . $d['url'])) {
if (!rename(dir_uploads . 'clients/' . $d['url'], dir_uploads . 'clients/' . $data[':url'])) {
trigger_error('Unable to rename client folder: "' . dir_uploads . 'clients/' . $d['url'] . '" -> "' . dir_uploads . 'clients/' . $data[':url'] . '"!');
}
} else {
if (!mkdir(dir_uploads . 'clients/' . $data[':url'])) {
trigger_error('Unable to create client folder: "' . dir_uploads . 'clients/' . $data[':url'] . '"');
}
}
}
}
} catch (PDOException $e) {
trigger_error("MySQL error: " . $e->getMessage());
file_put_contents(ini_get('error_log'), print_r($data, true), FILE_APPEND);
}
unset($data[':country']);
$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);
if ($write_log === true) {
if ($client_exists) {
file_put_contents(dir_root . 'webhook.txt', 'data updated' . "\n", FILE_APPEND);
} else {
file_put_contents(dir_root . 'webhook.txt', 'new data inserted' . "\n", FILE_APPEND);
}
}
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 get_country_id($country_code)
{
global $pdo;
$p = [];
try {
$p = [':iso' => strtoupper(trim($country_code))];
$db = $pdo->prepare("SELECT
`system_lib_countries`.`id`
FROM `system_lib_countries`
WHERE `system_lib_countries`.`iso`=:iso");
$db->execute($p);
if ($db->rowCount() > 0) {
return $db->fetchColumn();
} else {
return 0;
}
} catch (PDOException $e) {
trigger_error("MySQL get country ID error: " . $e->getMessage());
file_put_contents(ini_get('error_log'), print_r($p, true), FILE_APPEND);
return 0;
}
}
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;
}
файл webhook.php, который прослушивает уведомления ExactOnline, а затем обрабатывает их:
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);
file_put_contents(dir_root . 'webhook.txt', 'request is valid?: "' . $authState . '"' . "\n", FILE_APPEND);
file_put_contents(dir_root . 'webhook.txt', 'timestamp: ' . date('Y-m-d H:i:s', time()) . "\n", FILE_APPEND);
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');
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) : '')/*,
':created' => (!empty($client->Created) ? date('Y-m-d H:i:s', substr(str_replace(array(
'/Date(',
')/'
), '', $client->Created), 0, -3)) : '')*/
];
save_client($data, true);
file_put_contents(dir_root . 'webhook.txt', print_r($data, true) . "\n\n", FILE_APPEND);
}
setValue('last_exact_online_sync', date('Y-m-d\TH:i:s', time()));
}
} catch (Exception $e) {
setValue('authorizationCode', '');
trigger_error('connection for webhook failed: ' . $e->getMessage());
die();
}
}
}