request(self::PASSPORT_ENDPOINT, $fileData); } public function identification($fileData): array { return $this->request(self::IDCARD_ENDPOINT, $fileData); } public function visa($fileData): array { return $this->request(self::VISA_ENDPOINT, $fileData); } private function request(string $endpoint, $fileData): array { if (!is_string($fileData) || $fileData === '') { return $this->error('Unable to read uploaded file'); } $config = $this->loadConfig(); if ($config['base_url'] === '' || $config['app_id'] === '' || $config['secret'] === '') { return $this->error('OCR config missing: app.OCR_BASE_URL, app.OCR_APP_ID and app.OCR_SECRET are required'); } try { $payload = $this->buildPayload($fileData, $config); } catch (Throwable $e) { return $this->error('Unable to build OCR payload: ' . $e->getMessage()); } $timestamp = (string) time(); $signature = hash_hmac('sha256', $config['app_id'] . "\n" . $timestamp, $config['secret']); $headers = [ 'Content-Type: application/json', 'Accept: application/json', 'X-App-Id: ' . $config['app_id'], 'X-Timestamp: ' . $timestamp, 'X-Signature: ' . $signature, ]; if ($config['origin'] !== '') { $headers[] = 'Origin: ' . $config['origin']; } $ch = curl_init($config['base_url'] . $endpoint); if ($ch === false) { return $this->error('Unable to initialize cURL'); } try { curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE), CURLOPT_TIMEOUT => $config['timeout'], CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'], ]); if (!$config['verify_ssl']) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } $response = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response === false) { return $this->error('OCR request failed: ' . curl_error($ch)); } $decoded = json_decode((string) $response, true); if (!is_array($decoded)) { return $this->error('OCR response is not valid JSON', [ 'http_code' => $httpCode, 'response' => substr((string) $response, 0, 300), ]); } return $decoded; } finally { curl_close($ch); } } private function buildPayload(string $fileData, array $config): array { $mimeType = $this->detectMimeType($fileData); $extension = $this->extensionForMimeType($mimeType); return [ 'filename' => 'upload.' . $extension, 'content_type' => $mimeType, 'image_base64' => 'data:' . $mimeType . ';base64,' . base64_encode($fileData), 'auto_rotate' => $config['auto_rotate'], 'return_debug' => $config['return_debug'], ]; } private function loadConfig(): array { $config = config('ocr') ?: []; return [ 'base_url' => rtrim((string) (config('app.OCR_BASE_URL') ?? ''), '/'), 'app_id' => (string) (config('app.OCR_APP_ID') ?? ''), 'secret' => (string) (config('app.OCR_SECRET') ?? ''), 'origin' => $this->resolveOrigin((string) (config('app.OCR_ORIGIN') ?? ($config['origin'] ?? ''))), 'timeout' => max(1, (int) ($config['timeout'] ?? 60)), 'connect_timeout' => max(1, (int) ($config['connect_timeout'] ?? 10)), 'auto_rotate' => $this->boolValue($config['auto_rotate'] ?? true), 'return_debug' => $this->boolValue($config['return_debug'] ?? false), 'verify_ssl' => $this->boolValue($config['verify_ssl'] ?? true), ]; } private function resolveOrigin(string $configuredOrigin): string { $configuredOrigin = trim($configuredOrigin); if ($configuredOrigin !== '') { return rtrim($configuredOrigin, '/'); } $requestOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? '')); if ($this->isValidOrigin($requestOrigin)) { return rtrim($requestOrigin, '/'); } $host = trim((string) ($_SERVER['HTTP_HOST'] ?? '')); if ($host === '' || !preg_match('/^[A-Za-z0-9.-]+(?::\d+)?$/', $host)) { return ''; } return $this->currentScheme() . '://' . $host; } private function isValidOrigin(string $origin): bool { if ($origin === '') { return false; } $parts = parse_url($origin); if (!is_array($parts)) { return false; } $scheme = strtolower((string) ($parts['scheme'] ?? '')); return in_array($scheme, ['http', 'https'], true) && !empty($parts['host']) && empty($parts['path']) && empty($parts['query']) && empty($parts['fragment']); } private function currentScheme(): string { $forwardedProto = trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')); if ($forwardedProto !== '') { $forwardedProto = strtolower(trim(explode(',', $forwardedProto)[0])); if (in_array($forwardedProto, ['http', 'https'], true)) { return $forwardedProto; } } $requestScheme = strtolower((string) ($_SERVER['REQUEST_SCHEME'] ?? '')); if (in_array($requestScheme, ['http', 'https'], true)) { return $requestScheme; } $https = strtolower((string) ($_SERVER['HTTPS'] ?? '')); return $https !== '' && $https !== 'off' ? 'https' : 'http'; } private function detectMimeType(string $fileData): string { if (!class_exists(\finfo::class)) { return 'image/jpeg'; } $finfo = new \finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->buffer($fileData); if (is_string($mimeType) && $mimeType !== '' && $mimeType !== 'application/octet-stream') { return $mimeType; } return 'image/jpeg'; } private function extensionForMimeType(string $mimeType): string { return match ($mimeType) { 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/bmp', 'image/x-ms-bmp' => 'bmp', default => 'jpg', }; } private function boolValue($value): bool { $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); return $parsed ?? (bool) $value; } private function error(string $message, array $data = []): array { $result = [ 'code' => 0, 'msg' => $message, ]; if ($data !== []) { $result['data'] = $data; } return $result; } }