model = new TshieldWebhookModel(); $this->logFilePath = $this->resolveLogFilePath($_ENV['TSHIELD_WEBHOOK_LOG_FILE'] ?? null); } public function __invoke(ServerRequestInterface $request) { $requestId = $this->generateRequestId(); $rawBody = (string)$request->getBody(); if ($rawBody === '') { $rawBody = (string)file_get_contents('php://input'); } $this->logReceipt($request, $requestId, $rawBody); $body = json_decode($rawBody, true); try { if (!is_array($body)) { $response = ResponseLib::sendFail('Invalid JSON payload.', [], 'E_VALIDATE')->withStatus(400); $this->logWebhook($request, $requestId, $rawBody, null, $response, ['result' => 'invalid_json']); return $response; } $statusDescription = $body['status']['status'] ?? null; if ($statusDescription !== 'Aprovado') { $response = ResponseLib::sendOk([ 'processed' => false, 'reason' => 'Status not approved. No action taken.', ], 'S_IGNORED'); $this->logWebhook($request, $requestId, $rawBody, $body, $response, [ 'result' => 'ignored', 'status_description' => $statusDescription, ]); return $response; } $externalNumber = $body['number'] ?? $body['token'] ?? null; if (!is_string($externalNumber) || trim($externalNumber) === '') { $response = ResponseLib::sendFail('Missing analysis number/token in payload.', [], 'E_VALIDATE')->withStatus(400); $this->logWebhook($request, $requestId, $rawBody, $body, $response, ['result' => 'missing_number']); return $response; } $updated = $this->model->approveByExternalId($externalNumber); if (!$updated) { $response = ResponseLib::sendFail( 'No user found for provided analysis number.', ['number' => $externalNumber], 'E_NOT_FOUND' )->withStatus(404); $this->logWebhook($request, $requestId, $rawBody, $body, $response, [ 'result' => 'not_found', 'number' => $externalNumber, ]); return $response; } $response = ResponseLib::sendOk([ 'processed' => true, 'number' => $externalNumber, ]); $this->logWebhook($request, $requestId, $rawBody, $body, $response, [ 'result' => 'updated', 'number' => $externalNumber, ]); return $response; } catch (\Throwable $e) { $response = ResponseLib::sendFail('Webhook processing error.', [ 'request_id' => $requestId, 'error' => $e->getMessage(), ], 'E_WEBHOOK')->withStatus(500); $this->logWebhook($request, $requestId, $rawBody, is_array($body) ? $body : null, $response, [ 'result' => 'exception', 'exception' => [ 'message' => $e->getMessage(), 'type' => get_class($e), ], ]); return $response; } } private function generateRequestId(): string { try { return bin2hex(random_bytes(16)); } catch (\Throwable $e) { return uniqid('tshield_', true); } } private function logWebhook( ServerRequestInterface $request, string $requestId, string $rawBody, ?array $decodedBody, ResponseInterface $response, array $extra = [] ): void { $ts = (new \DateTimeImmutable())->format('c'); $serverParams = $request->getServerParams(); $ip = $serverParams['REMOTE_ADDR'] ?? null; $method = $request->getMethod(); $uri = (string)$request->getUri(); $responseBody = $this->getResponseBodyForLog($response); $entry = [ 'ts' => $ts, 'service' => 'tshield_webhook', 'request_id' => $requestId, 'method' => $method, 'uri' => $uri, 'ip' => $ip, 'request_raw' => $rawBody, 'request_json' => $decodedBody, 'response_status' => $response->getStatusCode(), 'response_body' => $responseBody, ]; if (!empty($extra)) { $entry['context'] = $extra; } $encoded = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($encoded === false) { $encoded = '{"ts":"' . $ts . '","service":"tshield_webhook","request_id":"' . $requestId . '","error":"unable_to_encode_log"}'; } try { @file_put_contents($this->logFilePath, $encoded . "\n", FILE_APPEND | LOCK_EX); } catch (\Throwable $e) { } error_log('[TShieldWebhook] ' . $encoded); } private function logReceipt(ServerRequestInterface $request, string $requestId, string $rawBody): void { $ts = (new \DateTimeImmutable())->format('c'); $serverParams = $request->getServerParams(); $ip = $serverParams['REMOTE_ADDR'] ?? null; $method = $request->getMethod(); $uri = (string)$request->getUri(); $entry = [ 'ts' => $ts, 'service' => 'tshield_webhook', 'event' => 'received', 'request_id' => $requestId, 'method' => $method, 'uri' => $uri, 'ip' => $ip, 'request_raw' => $rawBody, ]; $encoded = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($encoded === false) { $encoded = '{"ts":"' . $ts . '","service":"tshield_webhook","event":"received","request_id":"' . $requestId . '","error":"unable_to_encode_log"}'; } try { @file_put_contents($this->logFilePath, $encoded . "\n", FILE_APPEND | LOCK_EX); } catch (\Throwable $e) { } error_log('[TShieldWebhook] ' . $encoded); } private function getResponseBodyForLog(ResponseInterface $response): ?string { try { $stream = $response->getBody(); if ($stream === null) { return null; } if (method_exists($stream, 'isSeekable') && $stream->isSeekable()) { $pos = $stream->tell(); $stream->rewind(); $contents = $stream->getContents(); $stream->seek($pos); return $contents; } return (string)$stream; } catch (\Throwable $e) { return null; } } private function resolveLogFilePath(?string $path): string { $path = trim((string)$path); if ($path === '') { $path = 'tshield_webhook.txt'; } if (!preg_match('/^(?:[A-Za-z]:\\\\|\\/)/', $path)) { $path = rtrim(dirname(__DIR__), '/\\') . DIRECTORY_SEPARATOR . $path; } $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } return $path; } }