*/ private const ALLOWED_EXTENSIONS = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'zip', 'rar', '7z', 'tar', 'gz', ]; public function __construct( private readonly ArtworkStatsService $stats, private readonly ArtworkOriginalFileLocator $originalFiles, ) {} public function __invoke(Request $request, int $id): BinaryFileResponse|RedirectResponse|Response { $artwork = Artwork::query()->find($id); if (! $artwork) { return $this->notFound(); } $resolution = $this->originalFiles->resolve($artwork); $filePath = (string) $resolution['path']; $objectKey = (string) $resolution['object_key']; $ext = strtolower((string) ($resolution['file_ext'] ?: pathinfo($filePath !== '' ? $filePath : $objectKey, PATHINFO_EXTENSION))); if (($filePath === '' && $objectKey === '') || ! in_array($ext, self::ALLOWED_EXTENSIONS, true)) { return $this->notFound(); } if (! $resolution['exists']) { Log::warning('Artwork original file missing for download.', [ 'artwork_id' => $artwork->id, 'ext' => $ext, 'download_status' => $artwork->download_status, 'canonical_local_exists' => $filePath !== '' && File::isFile($filePath), 'object_exists' => false, 'resolution_status' => $resolution['status'], ]); return $this->notFound(); } $downloadName = $this->buildDownloadFilename((string) $artwork->file_name, $ext); $temporaryUrl = null; if ($resolution['source'] === 'object') { try { $temporaryUrl = Storage::disk((string) $resolution['disk'])->temporaryUrl( $objectKey, now()->addMinutes(5), [ 'ResponseContentDisposition' => 'attachment; filename="'.addslashes($downloadName).'"', ], ); } catch (\Throwable $exception) { Log::warning('Artwork object temporary URL generation failed.', [ 'artwork_id' => $artwork->id, 'disk' => $resolution['disk'], 'object_key' => $objectKey, 'object_exists' => true, 'object_size' => $resolution['size'], 'exception_class' => $exception::class, 'exception_message' => mb_substr($exception->getMessage(), 0, 300), ]); return $this->notFound(); } if (! is_string($temporaryUrl) || trim($temporaryUrl) === '') { Log::warning('Artwork object temporary URL generation failed.', [ 'artwork_id' => $artwork->id, 'disk' => $resolution['disk'], 'object_key' => $objectKey, 'resolution_status' => $resolution['status'], 'object_exists' => true, 'object_size' => $resolution['size'], 'exception_class' => 'InvalidTemporaryUrl', 'exception_message' => 'Storage adapter did not return a temporary URL.', ]); return $this->notFound(); } } $this->recordDownload($request, $artwork->id); $this->incrementDownloadCountIfAvailable($artwork->id); try { $this->stats->incrementDownloads((int) $artwork->id, 1, defer: false); } catch (\Throwable $exception) { Log::warning('Failed to increment artwork_stats download counter.', [ 'artwork_id' => $artwork->id, 'error' => $exception->getMessage(), ]); } if (array_key_exists('download_status', $artwork->getAttributes())) { $artwork->forceFill([ 'download_status' => 'available', 'download_source' => $resolution['source'], 'download_checked_at' => now(), ])->saveQuietly(); } // X-Accel-Redirect is safe only when nginx is explicitly configured to // map the internal URI to the originals root. Otherwise fallback to the // normal Laravel download response. $accelUri = $this->resolveAccelUri($filePath); if ($accelUri !== null) { return response('', 200, [ 'X-Accel-Redirect' => $accelUri, 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="'.addslashes($downloadName).'"', 'X-Content-Type-Options' => 'nosniff', ]); } if ($resolution['source'] === 'object') { return redirect()->away($temporaryUrl); } return response()->download($filePath, $downloadName); } private function notFound(): Response { return response('', 404, [ 'Content-Type' => 'text/plain; charset=UTF-8', 'Cache-Control' => 'no-store', ]); } private function resolveAccelUri(string $filePath): ?string { if (! config('app.download_accel_enabled')) { return null; } $accelBase = rtrim((string) config('app.download_accel_path', ''), '/'); if ($accelBase === '') { return null; } $root = rtrim((string) config('uploads.local_originals_root'), DIRECTORY_SEPARATOR); if ($root === '') { return null; } $normalizedRoot = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $root); $normalizedFilePath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $filePath); $rootPrefix = $normalizedRoot.DIRECTORY_SEPARATOR; if (! str_starts_with($normalizedFilePath, $rootPrefix)) { Log::warning('Artwork download accel path skipped because file is outside originals root.', [ 'resolved_path' => $filePath, 'originals_root' => $root, ]); return null; } $relativePath = substr($normalizedFilePath, strlen($normalizedRoot)); if ($relativePath === false || $relativePath === '') { return null; } return $accelBase.str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); } private function recordDownload(Request $request, int $artworkId): void { try { $ipAddress = $request->ip(); $ipBinary = $ipAddress ? @inet_pton($ipAddress) : false; ArtworkDownload::query()->create([ 'artwork_id' => $artworkId, 'user_id' => $request->user()?->id, 'ip' => $ipBinary !== false ? $ipBinary : null, 'ip_address' => $ipAddress, 'user_agent' => mb_substr((string) $request->userAgent(), 0, 1024), 'referer' => mb_substr((string) $request->headers->get('referer'), 0, 65535), ]); } catch (\Throwable $exception) { Log::warning('Failed to record artwork download analytics.', [ 'artwork_id' => $artworkId, 'error' => $exception->getMessage(), ]); } } private function incrementDownloadCountIfAvailable(int $artworkId): void { if (! Schema::hasColumn('artworks', 'download_count')) { return; } Artwork::query()->whereKey($artworkId)->increment('download_count'); } private function buildDownloadFilename(string $fileName, string $ext): string { $name = trim($fileName); $name = str_replace(['/', '\\'], '-', $name); $name = preg_replace('/[\x00-\x1F\x7F]/', '', $name) ?? ''; $name = preg_replace('/\s+/', ' ', $name) ?? ''; $name = trim((string) $name, ". \t\n\r\0\x0B"); if ($name === '') { $name = 'artwork'; } $baseName = pathinfo($name, PATHINFO_FILENAME); $baseName = trim((string) $baseName, ". \t\n\r\0\x0B"); if ($baseName === '') { $baseName = 'artwork'; } $brandSuffix = $this->downloadBrandSuffix(); if ($brandSuffix !== '' && ! Str::contains(Str::lower($baseName), Str::lower($brandSuffix))) { $baseName .= ' ('.$brandSuffix.')'; } return $baseName.'.'.$ext; } private function downloadBrandSuffix(): string { $host = (string) parse_url((string) config('app.url'), PHP_URL_HOST); $host = strtolower(trim($host)); $host = preg_replace('/^www\./', '', $host) ?? ''; if ($host === '' || in_array($host, ['localhost', '127.0.0.1'], true) || str_ends_with($host, '.test')) { return 'skinbase.org'; } return $host; } }