|
|
@@ -13,16 +13,33 @@ class DocumentUploadController
|
|
|
private DocumentModel $documentModel;
|
|
|
private DocumentStorageService $storage;
|
|
|
private int $maxUploadBytes;
|
|
|
+ private bool $debug;
|
|
|
|
|
|
public function __construct()
|
|
|
{
|
|
|
$this->documentModel = new DocumentModel();
|
|
|
$this->storage = new DocumentStorageService();
|
|
|
$this->maxUploadBytes = 30 * 1024 * 1024;
|
|
|
+ $this->debug = filter_var($_ENV['DOCUMENT_UPLOAD_DEBUG'] ?? 'true', FILTER_VALIDATE_BOOLEAN);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function log(string $requestId, string $message, array $context = []): void
|
|
|
+ {
|
|
|
+ if (!$this->debug) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $json = '';
|
|
|
+ if (!empty($context)) {
|
|
|
+ $json = ' ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
+ }
|
|
|
+
|
|
|
+ error_log('[documents.upload][' . $requestId . '] ' . $message . $json);
|
|
|
}
|
|
|
|
|
|
public function __invoke(ServerRequestInterface $request)
|
|
|
{
|
|
|
+ $requestId = bin2hex(random_bytes(4));
|
|
|
$userId = (int)($request->getAttribute('api_user_id') ?? 0);
|
|
|
$companyId = (int)($request->getAttribute('api_company_id') ?? 0);
|
|
|
|
|
|
@@ -32,26 +49,90 @@ class DocumentUploadController
|
|
|
|
|
|
$contentLength = (int)$request->getHeaderLine('Content-Length');
|
|
|
if ($contentLength > 0 && $contentLength > $this->maxUploadBytes) {
|
|
|
- return ResponseLib::sendFail('File too large. Max 30MB.', [], 'E_TOO_LARGE')->withStatus(413);
|
|
|
+ $this->log($requestId, 'content-length exceeded', [
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'company_id' => $companyId,
|
|
|
+ 'content_length' => $contentLength,
|
|
|
+ 'content_type' => $request->getHeaderLine('Content-Type'),
|
|
|
+ 'user_agent' => $request->getHeaderLine('User-Agent'),
|
|
|
+ 'x_forwarded_for' => $request->getHeaderLine('X-Forwarded-For'),
|
|
|
+ 'x_forwarded_proto' => $request->getHeaderLine('X-Forwarded-Proto'),
|
|
|
+ ]);
|
|
|
+ return ResponseLib::sendFail('File too large. Max 30MB.', ['request_id' => $requestId], 'E_TOO_LARGE')->withStatus(413);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
$parsed = MultipartFormDataParser::parse($request);
|
|
|
} catch (\Throwable $e) {
|
|
|
- return ResponseLib::sendFail('Invalid multipart form-data: ' . $e->getMessage(), [], 'E_VALIDATE')->withStatus(400);
|
|
|
+ $this->log($requestId, 'multipart parse failed', [
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'company_id' => $companyId,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'content_type' => $request->getHeaderLine('Content-Type'),
|
|
|
+ 'content_length' => $request->getHeaderLine('Content-Length'),
|
|
|
+ 'transfer_encoding' => $request->getHeaderLine('Transfer-Encoding'),
|
|
|
+ 'user_agent' => $request->getHeaderLine('User-Agent'),
|
|
|
+ 'x_forwarded_for' => $request->getHeaderLine('X-Forwarded-For'),
|
|
|
+ 'x_forwarded_proto' => $request->getHeaderLine('X-Forwarded-Proto'),
|
|
|
+ ]);
|
|
|
+ return ResponseLib::sendFail('Invalid multipart form-data: ' . $e->getMessage(), ['request_id' => $requestId], 'E_VALIDATE')->withStatus(400);
|
|
|
}
|
|
|
|
|
|
$fields = $parsed['fields'] ?? [];
|
|
|
$files = $parsed['files'] ?? [];
|
|
|
+ $meta = $parsed['meta'] ?? [];
|
|
|
|
|
|
$documentType = isset($fields['document_type']) ? (string)$fields['document_type'] : '';
|
|
|
if ($documentType === '') {
|
|
|
- return ResponseLib::sendFail('Missing field: document_type', [], 'E_VALIDATE')->withStatus(400);
|
|
|
+ $this->log($requestId, 'missing document_type', [
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'company_id' => $companyId,
|
|
|
+ 'field_names' => array_keys($fields),
|
|
|
+ 'file_names' => array_keys($files),
|
|
|
+ 'meta' => $meta,
|
|
|
+ 'content_type' => $request->getHeaderLine('Content-Type'),
|
|
|
+ 'content_length' => $request->getHeaderLine('Content-Length'),
|
|
|
+ 'transfer_encoding' => $request->getHeaderLine('Transfer-Encoding'),
|
|
|
+ 'user_agent' => $request->getHeaderLine('User-Agent'),
|
|
|
+ 'x_forwarded_for' => $request->getHeaderLine('X-Forwarded-For'),
|
|
|
+ 'x_forwarded_proto' => $request->getHeaderLine('X-Forwarded-Proto'),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $data = ['request_id' => $requestId];
|
|
|
+ if ($this->debug) {
|
|
|
+ $data['debug'] = [
|
|
|
+ 'field_names' => array_keys($fields),
|
|
|
+ 'file_names' => array_keys($files),
|
|
|
+ 'meta' => $meta,
|
|
|
+ 'content_type' => $request->getHeaderLine('Content-Type'),
|
|
|
+ 'content_length' => $request->getHeaderLine('Content-Length'),
|
|
|
+ 'transfer_encoding' => $request->getHeaderLine('Transfer-Encoding'),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return ResponseLib::sendFail('Missing field: document_type', $data, 'E_VALIDATE')->withStatus(400);
|
|
|
}
|
|
|
|
|
|
$file = $files['file'] ?? null;
|
|
|
if (!is_array($file) || !isset($file['content'])) {
|
|
|
- return ResponseLib::sendFail('Missing file field: file', [], 'E_VALIDATE')->withStatus(400);
|
|
|
+ $this->log($requestId, 'missing file', [
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'company_id' => $companyId,
|
|
|
+ 'field_names' => array_keys($fields),
|
|
|
+ 'file_names' => array_keys($files),
|
|
|
+ 'meta' => $meta,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $data = ['request_id' => $requestId];
|
|
|
+ if ($this->debug) {
|
|
|
+ $data['debug'] = [
|
|
|
+ 'field_names' => array_keys($fields),
|
|
|
+ 'file_names' => array_keys($files),
|
|
|
+ 'meta' => $meta,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return ResponseLib::sendFail('Missing file field: file', $data, 'E_VALIDATE')->withStatus(400);
|
|
|
}
|
|
|
|
|
|
$originalFilename = (string)($file['filename'] ?? 'upload.bin');
|
|
|
@@ -59,7 +140,7 @@ class DocumentUploadController
|
|
|
$content = (string)$file['content'];
|
|
|
|
|
|
if (strlen($content) > $this->maxUploadBytes) {
|
|
|
- return ResponseLib::sendFail('File too large. Max 30MB.', [], 'E_TOO_LARGE')->withStatus(413);
|
|
|
+ return ResponseLib::sendFail('File too large. Max 30MB.', ['request_id' => $requestId], 'E_TOO_LARGE')->withStatus(413);
|
|
|
}
|
|
|
|
|
|
try {
|