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, 'log_file' => $this->logFilePath, '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"}'; } $this->appendLogLine($encoded); 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, 'log_file' => $this->logFilePath, '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"}'; } $this->appendLogLine($encoded); error_log('[TShieldWebhook] ' . $encoded); } private function appendLogLine(string $encoded): bool { $phpError = null; $handler = function (int $severity, string $message) use (&$phpError): bool { $phpError = $message; return true; }; set_error_handler($handler); try { $result = file_put_contents($this->logFilePath, $encoded . "\n", FILE_APPEND | LOCK_EX); } catch (\Throwable $e) { $result = false; $phpError = $phpError ?? ($e->getMessage()); } finally { restore_error_handler(); } if ($result === false) { error_log('[TShieldWebhook] failed_to_write_log file=' . $this->logFilePath . ' php_error=' . json_encode($phpError)); return false; } return true; } 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)) { if (!@mkdir($dir, 0775, true) && !is_dir($dir)) { $lastError = error_get_last(); error_log('[TShieldWebhook] failed_to_create_log_dir dir=' . $dir . ' error=' . json_encode($lastError)); } } if (is_dir($dir) && !is_writable($dir)) { error_log('[TShieldWebhook] log_dir_not_writable dir=' . $dir); } if (!file_exists($path)) { $phpError = null; $handler = function (int $severity, string $message) use (&$phpError): bool { $phpError = $message; return true; }; set_error_handler($handler); try { $created = (file_put_contents($path, '') !== false); } catch (\Throwable $e) { $created = false; $phpError = $phpError ?? ($e->getMessage()); } finally { restore_error_handler(); } if (!$created) { error_log('[TShieldWebhook] failed_to_create_log_file file=' . $path . ' php_error=' . json_encode($phpError)); } } elseif (!is_writable($path)) { error_log('[TShieldWebhook] log_file_not_writable file=' . $path); } return $path; } }