343 lines
11 KiB
PHP
343 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* HTTP client for Shayegan Cyber Account webservice (REST JSON).
|
|
* Contract from vendor PDF: Config + Domain|PutObject; always inspect ErrorMessage.
|
|
*/
|
|
class ShayeganClient
|
|
{
|
|
/** @var ConfigService */
|
|
private $config;
|
|
|
|
public function __construct(ConfigService $config)
|
|
{
|
|
$this->config = $config;
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->baseUrl() !== ''
|
|
&& $this->config->getString('shayegan_sql_server', '') !== ''
|
|
&& $this->config->getString('shayegan_database', '') !== ''
|
|
&& $this->config->getString('shayegan_db_user', '') !== '';
|
|
}
|
|
|
|
public function baseUrl(): string
|
|
{
|
|
return rtrim(trim($this->config->getString('shayegan_base_url', '')), '/');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function buildConfig(): array
|
|
{
|
|
$authUser = trim($this->config->getString('shayegan_auth_user', ''));
|
|
$authPass = (string) $this->config->getString('shayegan_auth_password', '');
|
|
|
|
return [
|
|
'Server' => $this->config->getString('shayegan_sql_server', ''),
|
|
'AllowRowSecurity' => false,
|
|
'Level' => 0,
|
|
'DBUserName' => $this->config->getString('shayegan_db_user', ''),
|
|
'DBPassword' => (string) $this->config->getString('shayegan_db_password', ''),
|
|
'DataBaseName' => $this->config->getString('shayegan_database', ''),
|
|
'AuthUser' => $authUser !== '' ? $authUser : '',
|
|
'AuthPassword' => $authUser !== '' ? $authPass : '',
|
|
'Language' => $this->config->getInt('shayegan_language', 3),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Read-only smoke test: Account/Get one page.
|
|
*
|
|
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
|
*/
|
|
public function healthCheck(): array
|
|
{
|
|
if (! $this->isConfigured()) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'تنظیمات اتصال شایگان ناقص است (base_url / Server / DataBase / DBUser).',
|
|
'http_status' => null,
|
|
'body' => null,
|
|
];
|
|
}
|
|
|
|
return $this->get('Account/Get', [], 0, 1);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $domain
|
|
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
|
*/
|
|
public function get(string $endpoint, array $domain = [], int $rowStart = 0, int $rowCount = 1): array
|
|
{
|
|
$payload = [
|
|
'Config' => $this->buildConfig(),
|
|
// JSON object {} — empty PHP array encodes as [] and Shayegan .NET null-refs
|
|
'Domain' => $domain === [] ? new \stdClass() : $domain,
|
|
];
|
|
|
|
return $this->request('GET_STYLE', $endpoint, $payload, [
|
|
'RowStart' => $rowStart,
|
|
'RowCount' => $rowCount,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $putObject
|
|
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool,number:?string,guid:?string}
|
|
*/
|
|
public function put(string $endpoint, array $putObject): array
|
|
{
|
|
$payload = [
|
|
'Config' => $this->buildConfig(),
|
|
'PutObject' => array_values($putObject),
|
|
];
|
|
|
|
$result = $this->request('PUT_STYLE', $endpoint, $payload, []);
|
|
if (! empty($result['body']) && is_array($result['body'])) {
|
|
$first = $this->firstResultRow($result['body']);
|
|
$result['number'] = isset($first['Number']) ? (string) $first['Number'] : null;
|
|
$result['guid'] = isset($first['GuId']) ? (string) $first['GuId'] : (isset($first['Guid']) ? (string) $first['Guid'] : null);
|
|
} else {
|
|
$result['number'] = null;
|
|
$result['guid'] = null;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, int|string> $query
|
|
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
|
*/
|
|
private function request(string $style, string $endpoint, array $payload, array $query): array
|
|
{
|
|
$url = $this->endpointUrl($endpoint, $query);
|
|
$timeout = max(5, $this->config->getInt('shayegan_timeout_seconds', 30));
|
|
|
|
$this->logTraffic('request', [
|
|
'style' => $style,
|
|
'endpoint' => $endpoint,
|
|
'url' => $url,
|
|
'timeout_seconds' => $timeout,
|
|
'payload' => $this->redactPayload($payload),
|
|
]);
|
|
|
|
try {
|
|
$response = Http::timeout($timeout)
|
|
->withHeaders(['Content-Type' => 'application/json', 'Accept' => 'application/json'])
|
|
->post($url, $payload);
|
|
|
|
$status = $response->status();
|
|
$json = $response->json();
|
|
if (! is_array($json)) {
|
|
$raw = (string) $response->body();
|
|
$decoded = json_decode($raw, true);
|
|
$json = is_array($decoded) ? $decoded : ['raw' => Str::limit($raw, 2000)];
|
|
}
|
|
|
|
$error = $this->extractErrorMessage($json);
|
|
if ($status >= 400) {
|
|
$result = [
|
|
'ok' => false,
|
|
'message' => $error !== '' ? $error : ('HTTP ' . $status),
|
|
'http_status' => $status,
|
|
'body' => $json,
|
|
];
|
|
$this->logTraffic('response', [
|
|
'style' => $style,
|
|
'endpoint' => $endpoint,
|
|
'url' => $url,
|
|
'ok' => false,
|
|
'http_status' => $status,
|
|
'error_message' => $error,
|
|
'body' => $this->redactBodyForLog($json),
|
|
]);
|
|
|
|
return $result;
|
|
}
|
|
|
|
if ($error !== '') {
|
|
$result = [
|
|
'ok' => false,
|
|
'message' => $error,
|
|
'http_status' => $status,
|
|
'body' => $json,
|
|
];
|
|
$this->logTraffic('response', [
|
|
'style' => $style,
|
|
'endpoint' => $endpoint,
|
|
'url' => $url,
|
|
'ok' => false,
|
|
'http_status' => $status,
|
|
'error_message' => $error,
|
|
'body' => $this->redactBodyForLog($json),
|
|
]);
|
|
|
|
return $result;
|
|
}
|
|
|
|
$result = [
|
|
'ok' => true,
|
|
'message' => 'OK',
|
|
'http_status' => $status,
|
|
'body' => $json,
|
|
];
|
|
$this->logTraffic('response', [
|
|
'style' => $style,
|
|
'endpoint' => $endpoint,
|
|
'url' => $url,
|
|
'ok' => true,
|
|
'http_status' => $status,
|
|
'error_message' => '',
|
|
'body' => $this->redactBodyForLog($json),
|
|
]);
|
|
|
|
return $result;
|
|
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
|
Log::warning('shayegan.timeout_or_connection', [
|
|
'endpoint' => $endpoint,
|
|
'style' => $style,
|
|
'url' => $url,
|
|
'message' => $e->getMessage(),
|
|
'payload' => $this->redactPayload($payload),
|
|
]);
|
|
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'timeout/connection: ' . $e->getMessage(),
|
|
'http_status' => null,
|
|
'body' => null,
|
|
'timeout' => true,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::warning('shayegan.request_failed', [
|
|
'endpoint' => $endpoint,
|
|
'style' => $style,
|
|
'url' => $url,
|
|
'message' => $e->getMessage(),
|
|
'payload' => $this->redactPayload($payload),
|
|
]);
|
|
|
|
return [
|
|
'ok' => false,
|
|
'message' => $e->getMessage(),
|
|
'http_status' => null,
|
|
'body' => null,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, int|string> $query
|
|
*/
|
|
private function endpointUrl(string $endpoint, array $query): string
|
|
{
|
|
$endpoint = ltrim(str_replace('\\', '/', $endpoint), '/');
|
|
$url = $this->baseUrl() . '/api/' . $endpoint;
|
|
if ($query !== []) {
|
|
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $body
|
|
*/
|
|
public function extractErrorMessage(array $body): string
|
|
{
|
|
$top = trim((string) ($body['ErrorMessage'] ?? ''));
|
|
if ($top !== '') {
|
|
return $top;
|
|
}
|
|
|
|
$row = $this->firstResultRow($body);
|
|
if ($row !== null) {
|
|
$rowError = trim((string) ($row['ErrorMessage'] ?? ''));
|
|
if ($rowError !== '') {
|
|
return $rowError;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $body
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function firstResultRow(array $body): ?array
|
|
{
|
|
$result = $body['Result'] ?? null;
|
|
if (! is_array($result) || $result === []) {
|
|
return null;
|
|
}
|
|
|
|
$first = isset($result[0]) && is_array($result[0]) ? $result[0] : null;
|
|
if ($first !== null) {
|
|
return $first;
|
|
}
|
|
|
|
return is_array($result) && ! isset($result[0]) ? $result : null;
|
|
}
|
|
|
|
/**
|
|
* Strip secrets before logging / storing in admin-visible res when needed.
|
|
*
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function redactPayload(array $payload): array
|
|
{
|
|
if (isset($payload['Config']) && is_array($payload['Config'])) {
|
|
foreach (['DBPassword', 'AuthPassword'] as $key) {
|
|
if (array_key_exists($key, $payload['Config']) && $payload['Config'][$key] !== '' && $payload['Config'][$key] !== null) {
|
|
$payload['Config'][$key] = '***';
|
|
}
|
|
}
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function logTraffic(string $phase, array $context): void
|
|
{
|
|
if ($this->config->getInt('shayegan_log_traffic', 1) === 0) {
|
|
return;
|
|
}
|
|
|
|
Log::info('shayegan.http_' . $phase, $context);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $body
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function redactBodyForLog(array $body): array
|
|
{
|
|
if (isset($body['Result']) && is_array($body['Result'])) {
|
|
$count = count($body['Result']);
|
|
$body['Result'] = array_slice($body['Result'], 0, 2);
|
|
if ($count > 2) {
|
|
$body['Result_truncated'] = $count - 2;
|
|
}
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
}
|