respondError('Invalid filename', Response::HTTP_BAD_REQUEST); } // 2) Allow-list extensions (optional but safer) $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (!in_array($ext, $this->allowedExtensions, true)) { throw new NotFoundHttpException(); } // 3) Build path under storage/app/uploads/receipts $path = $this->storagePath("uploads/receipts/{$name}"); if (!is_file($path)) { throw new NotFoundHttpException(); } return $this->streamFile($path, $name); } public function reimb(string $name) { // 1) Path traversal guard if ($name !== basename($name)) { return $this->respondError('Invalid filename', Response::HTTP_BAD_REQUEST); } // 2) Allow-list extensions $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (!in_array($ext, $this->allowedExtensions, true)) { throw new NotFoundHttpException(); } // 3) Build path under storage/app/uploads/reimbursements $path = $this->storagePath("uploads/reimbursements/{$name}"); if (!is_file($path)) { throw new NotFoundHttpException(); } return $this->streamFile($path, $name); } /** * Alias for reimb() for backward compatibility */ public function reimbursement(string $name) { return $this->reimb($name); } public function index() { $type = $this->request->getGet('type') ?? 'receipts'; $allowedTypes = ['receipts', 'reimbursements', 'checks']; if (!in_array($type, $allowedTypes, true)) { return $this->respondError('Invalid file type', Response::HTTP_BAD_REQUEST); } try { $path = $this->storagePath("uploads/{$type}/"); $files = []; if (is_dir($path)) { foreach (scandir($path) as $file) { if ($file === '.' || $file === '..') { continue; } $filePath = $path . $file; if (!is_file($filePath)) { continue; } $files[] = [ 'name' => $file, 'size' => filesize($filePath), 'modified' => date('Y-m-d H:i:s', filemtime($filePath)), ]; } } return $this->success([ 'type' => $type, 'files' => $files, 'count' => count($files), ], 'Files retrieved successfully'); } catch (\Throwable $e) { log_message('error', 'Files list error: ' . $e->getMessage()); return $this->respondError('Failed to retrieve files', Response::HTTP_INTERNAL_SERVER_ERROR); } } protected function streamFile(string $path, string $name): HttpResponse { // 4) MIME detection (finfo is more reliable than mime_content_type) $mime = 'application/octet-stream'; if (function_exists('finfo_open')) { $fi = finfo_open(FILEINFO_MIME_TYPE); if ($fi) { $detected = finfo_file($fi, $path); if ($detected) { $mime = $detected; } finfo_close($fi); } } elseif (function_exists('mime_content_type')) { $mime = mime_content_type($path) ?: $mime; } // 5) Caching (ETag + Last-Modified) and 304 support $mtime = filemtime($path) ?: time(); $size = filesize($path) ?: 0; $etag = md5($name . '|' . $mtime . '|' . $size); $ifNoneMatch = trim($this->laravelRequest->header('If-None-Match', ''), '"'); $ifModifiedSince = $this->laravelRequest->header('If-Modified-Since'); $imsTime = $ifModifiedSince ? strtotime($ifModifiedSince) : false; if (($ifNoneMatch && $ifNoneMatch === $etag) || ($imsTime !== false && $imsTime >= $mtime) ) { return response('', Response::HTTP_NOT_MODIFIED) ->header('ETag', $etag) ->header('Last-Modified', gmdate('D, d M Y H:i:s', $mtime) . ' GMT'); } // 6) Stream the file inline return response()->make(file_get_contents($path), Response::HTTP_OK, [ 'Content-Type' => $mime, 'X-Content-Type-Options' => 'nosniff', 'Content-Disposition' => 'inline; filename="' . $name . '"', 'Content-Length' => (string) $size, 'ETag' => $etag, 'Last-Modified' => gmdate('D, d M Y H:i:s', $mtime) . ' GMT', 'Cache-Control' => 'public, max-age=86400', ]); } protected function storagePath(string $relative): string { return storage_path('app/' . $relative); } protected function isAllowedExtension(string $name): bool { $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); return in_array($ext, $this->allowedExtensions, true); } protected function detectMime(string $path): string { if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo) { $mime = finfo_file($finfo, $path); finfo_close($finfo); if ($mime) { return $mime; } } } elseif (function_exists('mime_content_type')) { $mime = mime_content_type($path); if ($mime) { return $mime; } } return 'application/octet-stream'; } }