From 7ce1e3487f3962bd120c92d1dfc52ecae9641386 Mon Sep 17 00:00:00 2001 From: ignacionelson Date: Fri, 18 Sep 2026 01:58:36 -0300 Subject: [PATCH] Tell whoever opens a public link that nothing checked the file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An installation that scans can still let files through: too large for the scanner, an archive it could not open, or an upload that arrived while the scanner was down. Both policies default to letting those through, and the count of them is on the settings screen and the dashboard. Everyone could see that except the one person it matters to. The uploader sees the state on their own file and staff see it in the library; whoever follows a public link saw the page a file that passed gets, having neither chosen the policy nor any way to see the setting. On the hosted free plan, where every upload is published behind a link, that is the whole audience. The share page and the public file page in all four themes now carry one line: "This file was not checked for viruses." Said plainly and without alarm — nothing is known to be wrong with the file; what is known is that nothing looked. Only where this installation scans, and only for the three reasons that mean a scanner let something past. A file from before scanning was switched on says nothing: on an installation that has only just switched it on that is every file, and saying it about all of them says it about none of them. --- .../Controllers/PublicShareController.php | 7 ++ app/Modules/Files/Models/File.php | 31 ++++- .../Controllers/PublicGroupsController.php | 4 + lang/ca.json | 3 +- lang/cs.json | 3 +- lang/de.json | 3 +- lang/es.json | 3 +- lang/fr.json | 3 +- lang/id.json | 3 +- lang/it.json | 3 +- lang/ja.json | 3 +- lang/nl.json | 3 +- lang/pl.json | 3 +- lang/pt_BR.json | 3 +- lang/ru.json | 3 +- lang/sw.json | 3 +- lang/tr.json | 3 +- lang/vi.json | 3 +- lang/zh_CN.json | 3 +- .../js/components/files/unscanned-notice.tsx | 38 ++++++ .../js/pages/public/themes/compact/file.tsx | 6 + .../js/pages/public/themes/default/file.tsx | 6 + .../js/pages/public/themes/drive/file.tsx | 6 + .../js/pages/public/themes/gallery/file.tsx | 6 + resources/js/pages/share/show.tsx | 7 +- tests/Feature/Files/UnscannedNoticeTest.php | 109 ++++++++++++++++++ 26 files changed, 246 insertions(+), 22 deletions(-) create mode 100644 resources/js/components/files/unscanned-notice.tsx create mode 100644 tests/Feature/Files/UnscannedNoticeTest.php diff --git a/app/Modules/Files/Http/Controllers/PublicShareController.php b/app/Modules/Files/Http/Controllers/PublicShareController.php index 4f41a96e..4ff68313 100644 --- a/app/Modules/Files/Http/Controllers/PublicShareController.php +++ b/app/Modules/Files/Http/Controllers/PublicShareController.php @@ -12,6 +12,7 @@ use App\Modules\Files\Delivery\StoredFileResponse; use App\Modules\Files\Models\Category; use App\Modules\Files\Models\File; use App\Modules\Files\Scanning\FileAvailability; +use App\Modules\Files\Scanning\ScanningConfig; use App\Modules\Files\Scanning\ScanStatus; use App\Modules\Files\Models\ShareLink; use Illuminate\Http\RedirectResponse; @@ -32,6 +33,7 @@ class PublicShareController extends Controller private readonly DownloadAllowance $allowance, private readonly StoredFileResponse $bytes, private readonly FileAvailability $availability, + private readonly ScanningConfig $scanning, ) {} public function show(string $token): InertiaResponse @@ -84,6 +86,11 @@ class PublicShareController extends Controller ])->values()->all(), ], 'download_url' => route('share.download', $token), + // Said to the one person who can neither see the setting nor + // chose it. The uploader and the staff library both show this + // file as "not scanned"; whoever follows the link had no way + // of knowing. + 'unscanned' => $this->scanning->enabled() && $file->wasLetThrough(), ]); } diff --git a/app/Modules/Files/Models/File.php b/app/Modules/Files/Models/File.php index cd7168b9..14e9477b 100644 --- a/app/Modules/Files/Models/File.php +++ b/app/Modules/Files/Models/File.php @@ -319,6 +319,21 @@ class File extends Model }); } + /** + * Why a file went out unchecked. Deliberately not + * NotScannedReason::BeforeScanning: a file stored while this + * installation did not scan at all is not a scanner letting something + * past, and on an installation that has never scanned it would mean + * saying it about every file there is. + * + * @var list + */ + private const LET_THROUGH_REASONS = [ + NotScannedReason::ScannerUnavailable->value, + NotScannedReason::TooLarge->value, + NotScannedReason::Encrypted->value, + ]; + /** * Files people can download that nothing checked: let through while * the scanner was down, or because it could not open them. @@ -334,11 +349,17 @@ class File extends Model public function scopeLetThrough(Builder $query): void { $query->where('scan_status', ScanStatus::NotScanned) - ->whereIn('scan_note', [ - NotScannedReason::ScannerUnavailable->value, - NotScannedReason::TooLarge->value, - NotScannedReason::Encrypted->value, - ]); + ->whereIn('scan_note', self::LET_THROUGH_REASONS); + } + + /** + * Whether this particular file is one of those — the row's own answer + * to scopeLetThrough(), for a page that already has the file. + */ + public function wasLetThrough(): bool + { + return $this->scan_status === ScanStatus::NotScanned + && in_array((string) $this->scan_note, self::LET_THROUGH_REASONS, true); } /** diff --git a/app/Modules/Groups/Http/Controllers/PublicGroupsController.php b/app/Modules/Groups/Http/Controllers/PublicGroupsController.php index 5a6da881..307e941c 100644 --- a/app/Modules/Groups/Http/Controllers/PublicGroupsController.php +++ b/app/Modules/Groups/Http/Controllers/PublicGroupsController.php @@ -14,6 +14,7 @@ use App\Modules\Files\Delivery\StoredFileResponse; use App\Modules\Files\Models\Category; use App\Modules\Files\Models\File; use App\Modules\Files\Scanning\FileAvailability; +use App\Modules\Files\Scanning\ScanningConfig; use App\Modules\Files\Models\Folder; use App\Modules\Files\Preview\PreviewKind; use App\Modules\Files\Preview\PreviewLog; @@ -228,6 +229,9 @@ class PublicGroupsController extends Controller // is allowed. 'preview_url' => $this->previewUrlFor($file, $publicSlug), 'download_url' => route('public.download', [$publicSlug, $file->slug]), + // See PublicShareController::show — the same sentence, to the + // same person, on the other public surface. + 'unscanned' => app(ScanningConfig::class)->enabled() && $file->wasLetThrough(), // Same decided shape the listings send, so a theme's single // file page disables its button for the same reason a row // does — see DownloadAllowance::summaryFor. diff --git a/lang/ca.json b/lang/ca.json index 94c0def5..897e15ae 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Un fitxer d'aquest arxiu ja no està disponible. Torna a descarregar la selecció.", "A scan is already running. Wait for it to finish.": "Ja hi ha una anàlisi en curs. Espera que acabi.", "Something answered at :address, but it is not a ClamAV scanner.": "Alguna cosa ha respost a :address, però no és un escàner ClamAV.", - "Available until :date": "Disponible fins al :date" + "Available until :date": "Disponible fins al :date", + "This file was not checked for viruses.": "Aquest fitxer no s'ha analitzat a la recerca de virus." } diff --git a/lang/cs.json b/lang/cs.json index 9f15ddd6..4759357b 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Soubor z tohoto archivu už není k dispozici. Stáhněte výběr znovu.", "A scan is already running. Wait for it to finish.": "Kontrola už probíhá. Počkejte, až skončí.", "Something answered at :address, but it is not a ClamAV scanner.": "Na adrese :address něco odpovědělo, ale není to skener ClamAV.", - "Available until :date": "K dispozici do :date" + "Available until :date": "K dispozici do :date", + "This file was not checked for viruses.": "Tento soubor nebyl zkontrolován na viry." } diff --git a/lang/de.json b/lang/de.json index 9ac6554a..9bb1d573 100644 --- a/lang/de.json +++ b/lang/de.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Eine Datei in diesem Archiv ist nicht mehr verfügbar. Lade die Auswahl erneut herunter.", "A scan is already running. Wait for it to finish.": "Es läuft bereits ein Scan. Warte, bis er fertig ist.", "Something answered at :address, but it is not a ClamAV scanner.": "Unter :address hat etwas geantwortet, aber es ist kein ClamAV-Scanner.", - "Available until :date": "Verfügbar bis :date" + "Available until :date": "Verfügbar bis :date", + "This file was not checked for viruses.": "Diese Datei wurde nicht auf Viren geprüft." } diff --git a/lang/es.json b/lang/es.json index a9a08d6d..f4b9ec42 100644 --- a/lang/es.json +++ b/lang/es.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Un archivo de este comprimido ya no está disponible. Vuelve a descargar la selección.", "A scan is already running. Wait for it to finish.": "Ya hay un análisis en curso. Espera a que termine.", "Something answered at :address, but it is not a ClamAV scanner.": "Algo respondió en :address, pero no es un analizador ClamAV.", - "Available until :date": "Disponible hasta el :date" + "Available until :date": "Disponible hasta el :date", + "This file was not checked for viruses.": "Este archivo no fue analizado en busca de virus." } diff --git a/lang/fr.json b/lang/fr.json index 691c9dde..5a2429b0 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Un fichier de cette archive n'est plus disponible. Téléchargez à nouveau la sélection.", "A scan is already running. Wait for it to finish.": "Une analyse est déjà en cours. Attendez qu'elle se termine.", "Something answered at :address, but it is not a ClamAV scanner.": "Quelque chose a répondu à :address, mais ce n'est pas un scanner ClamAV.", - "Available until :date": "Disponible jusqu'au :date" + "Available until :date": "Disponible jusqu'au :date", + "This file was not checked for viruses.": "Ce fichier n'a pas été analysé à la recherche de virus." } diff --git a/lang/id.json b/lang/id.json index 62ae56ff..ea0918c7 100644 --- a/lang/id.json +++ b/lang/id.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Sebuah file dalam arsip ini sudah tidak tersedia. Unduh ulang pilihan tersebut.", "A scan is already running. Wait for it to finish.": "Pemindaian sudah berjalan. Tunggu hingga selesai.", "Something answered at :address, but it is not a ClamAV scanner.": "Ada yang menjawab di :address, tetapi itu bukan pemindai ClamAV.", - "Available until :date": "Tersedia hingga :date" + "Available until :date": "Tersedia hingga :date", + "This file was not checked for viruses.": "Berkas ini belum dipindai dari virus." } diff --git a/lang/it.json b/lang/it.json index 9108d7af..44c159a2 100644 --- a/lang/it.json +++ b/lang/it.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Un file di questo archivio non è più disponibile. Scarica di nuovo la selezione.", "A scan is already running. Wait for it to finish.": "È già in corso una scansione. Attendi che finisca.", "Something answered at :address, but it is not a ClamAV scanner.": "Qualcosa ha risposto su :address, ma non è uno scanner ClamAV.", - "Available until :date": "Disponibile fino al :date" + "Available until :date": "Disponibile fino al :date", + "This file was not checked for viruses.": "Questo file non è stato controllato alla ricerca di virus." } diff --git a/lang/ja.json b/lang/ja.json index c3892cae..87cfb509 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "このアーカイブ内のファイルは利用できなくなりました。もう一度選択をダウンロードしてください。", "A scan is already running. Wait for it to finish.": "スキャンはすでに実行中です。終了するまでお待ちください。", "Something answered at :address, but it is not a ClamAV scanner.": ":address で応答がありましたが、ClamAV スキャナーではありません。", - "Available until :date": ":date まで利用可能" + "Available until :date": ":date まで利用可能", + "This file was not checked for viruses.": "このファイルはウイルス検査を受けていません。" } diff --git a/lang/nl.json b/lang/nl.json index d4c981de..69112b43 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Een bestand in dit archief is niet meer beschikbaar. Download de selectie opnieuw.", "A scan is already running. Wait for it to finish.": "Er loopt al een scan. Wacht tot die klaar is.", "Something answered at :address, but it is not a ClamAV scanner.": "Er antwoordde iets op :address, maar het is geen ClamAV-scanner.", - "Available until :date": "Beschikbaar tot :date" + "Available until :date": "Beschikbaar tot :date", + "This file was not checked for viruses.": "Dit bestand is niet op virussen gecontroleerd." } diff --git a/lang/pl.json b/lang/pl.json index 25a7f066..b49a7ed6 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Plik z tego archiwum nie jest już dostępny. Pobierz zaznaczenie ponownie.", "A scan is already running. Wait for it to finish.": "Skanowanie już trwa. Poczekaj, aż się zakończy.", "Something answered at :address, but it is not a ClamAV scanner.": "Coś odpowiedziało pod adresem :address, ale to nie jest skaner ClamAV.", - "Available until :date": "Dostępny do :date" + "Available until :date": "Dostępny do :date", + "This file was not checked for viruses.": "Ten plik nie został sprawdzony pod kątem wirusów." } diff --git a/lang/pt_BR.json b/lang/pt_BR.json index bdeef033..1c36933d 100644 --- a/lang/pt_BR.json +++ b/lang/pt_BR.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Um arquivo deste pacote não está mais disponível. Baixe a seleção novamente.", "A scan is already running. Wait for it to finish.": "Já há uma verificação em andamento. Aguarde até que termine.", "Something answered at :address, but it is not a ClamAV scanner.": "Algo respondeu em :address, mas não é um scanner ClamAV.", - "Available until :date": "Disponível até :date" + "Available until :date": "Disponível até :date", + "This file was not checked for viruses.": "Este arquivo não foi verificado em busca de vírus." } diff --git a/lang/ru.json b/lang/ru.json index 1fc1b592..ff108942 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Один из файлов этого архива больше недоступен. Скачайте выбранное заново.", "A scan is already running. Wait for it to finish.": "Проверка уже идёт. Дождитесь её завершения.", "Something answered at :address, but it is not a ClamAV scanner.": "По адресу :address что-то ответило, но это не сканер ClamAV.", - "Available until :date": "Доступен до :date" + "Available until :date": "Доступен до :date", + "This file was not checked for viruses.": "Этот файл не проверялся на вирусы." } diff --git a/lang/sw.json b/lang/sw.json index 58cbf719..2335363a 100644 --- a/lang/sw.json +++ b/lang/sw.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Faili moja katika kumbukumbu hii halipatikani tena. Pakua uteuzi upya.", "A scan is already running. Wait for it to finish.": "Uchanganuzi tayari unaendelea. Subiri umalizike.", "Something answered at :address, but it is not a ClamAV scanner.": "Kitu kimejibu kwenye :address, lakini si kichanganuzi cha ClamAV.", - "Available until :date": "Inapatikana hadi :date" + "Available until :date": "Inapatikana hadi :date", + "This file was not checked for viruses.": "Faili hii haijakaguliwa dhidi ya virusi." } diff --git a/lang/tr.json b/lang/tr.json index 86ed60df..3342774f 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Bu arşivdeki bir dosya artık kullanılamıyor. Seçimi yeniden indirin.", "A scan is already running. Wait for it to finish.": "Zaten bir tarama çalışıyor. Bitmesini bekleyin.", "Something answered at :address, but it is not a ClamAV scanner.": ":address adresinde bir şey yanıt verdi, ancak bu bir ClamAV tarayıcısı değil.", - "Available until :date": ":date tarihine kadar kullanılabilir" + "Available until :date": ":date tarihine kadar kullanılabilir", + "This file was not checked for viruses.": "Bu dosya virüslere karşı taranmadı." } diff --git a/lang/vi.json b/lang/vi.json index 6118ae19..3b8669a8 100644 --- a/lang/vi.json +++ b/lang/vi.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "Một tệp trong gói nén này không còn khả dụng. Hãy tải lại phần đã chọn.", "A scan is already running. Wait for it to finish.": "Đã có một lượt quét đang chạy. Hãy đợi nó hoàn tất.", "Something answered at :address, but it is not a ClamAV scanner.": "Có dịch vụ trả lời tại :address, nhưng đó không phải là trình quét ClamAV.", - "Available until :date": "Có sẵn đến :date" + "Available until :date": "Có sẵn đến :date", + "This file was not checked for viruses.": "Tệp này chưa được quét virus." } diff --git a/lang/zh_CN.json b/lang/zh_CN.json index 2c195a1b..bf6f61c9 100644 --- a/lang/zh_CN.json +++ b/lang/zh_CN.json @@ -2249,5 +2249,6 @@ "A file in this archive is no longer available. Download the selection again.": "此压缩包中的某个文件已不可用。请重新下载所选内容。", "A scan is already running. Wait for it to finish.": "已有扫描正在进行。请等待其完成。", "Something answered at :address, but it is not a ClamAV scanner.": ":address 上有程序应答,但它不是 ClamAV 扫描器。", - "Available until :date": "可用至 :date" + "Available until :date": "可用至 :date", + "This file was not checked for viruses.": "此文件未经过病毒检查。" } diff --git a/resources/js/components/files/unscanned-notice.tsx b/resources/js/components/files/unscanned-notice.tsx new file mode 100644 index 00000000..d1dfe466 --- /dev/null +++ b/resources/js/components/files/unscanned-notice.tsx @@ -0,0 +1,38 @@ +import { ShieldQuestion } from 'lucide-react'; + +import { useTranslation } from '@/hooks/use-translation'; +import { cn } from '@/lib/utils'; + +interface UnscannedNoticeProps { + /** Whether this file went out without being checked. Decided by the server. */ + unscanned?: boolean; + className?: string; +} + +/** + * Told to whoever opens a public link: this file was never checked. + * + * It happens on an installation that scans and chose to let files through + * anyway — too large for the scanner, an archive it could not open, or an + * upload that arrived while the scanner was down. The uploader and the + * staff library both see that state on the file; the person following the + * link neither chose the policy nor can see the setting, and until this + * they were the only one with no signal at all. + * + * Stated plainly and without alarm: nothing is known to be wrong with the + * file. What is known is that nothing looked. + */ +export function UnscannedNotice({ unscanned, className }: UnscannedNoticeProps) { + const { t } = useTranslation(); + + if (!unscanned) { + return null; + } + + return ( +

+ + {t('This file was not checked for viruses.')} +

+ ); +} diff --git a/resources/js/pages/public/themes/compact/file.tsx b/resources/js/pages/public/themes/compact/file.tsx index 4091e077..30d3b9a2 100644 --- a/resources/js/pages/public/themes/compact/file.tsx +++ b/resources/js/pages/public/themes/compact/file.tsx @@ -3,6 +3,7 @@ import { File as FileIcon } from 'lucide-react'; import { CommentsShellCompact } from '@/components/comments/shells/comments-shell-compact'; import { DownloadAction } from '@/components/download-action'; +import { UnscannedNotice } from '@/components/files/unscanned-notice'; import { PreviewAction } from '@/components/preview-action'; import { CategoryBadges, type CategoryTag } from '@/components/files/category-badges'; import { type VersionLinks } from '@/components/files/version-badge'; @@ -33,6 +34,8 @@ interface PublicFileShowProps { preview_url: string | null; download_url: string; download_limit: DownloadLimit; + /** Whether the file went out unchecked — see UnscannedNotice. */ + unscanned?: boolean; comments_enabled: boolean; comments_endpoint: string; } @@ -43,6 +46,7 @@ export default function PublicFileShowCompact({ preview_url, download_url, download_limit, + unscanned, comments_enabled, comments_endpoint, }: PublicFileShowProps) { @@ -86,6 +90,8 @@ export default function PublicFileShowCompact({ /> + + diff --git a/resources/js/pages/public/themes/default/file.tsx b/resources/js/pages/public/themes/default/file.tsx index d693cd6c..c177bdcc 100644 --- a/resources/js/pages/public/themes/default/file.tsx +++ b/resources/js/pages/public/themes/default/file.tsx @@ -3,6 +3,7 @@ import { File as FileIcon } from 'lucide-react'; import { CommentsShellDefault } from '@/components/comments/shells/comments-shell-default'; import { DownloadAction } from '@/components/download-action'; +import { UnscannedNotice } from '@/components/files/unscanned-notice'; import { PreviewAction } from '@/components/preview-action'; import { CategoryBadges, type CategoryTag } from '@/components/files/category-badges'; import { type VersionLinks } from '@/components/files/version-badge'; @@ -33,6 +34,8 @@ interface PublicFileShowProps { preview_url: string | null; download_url: string; download_limit: DownloadLimit; + /** Whether the file went out unchecked — see UnscannedNotice. */ + unscanned?: boolean; comments_enabled: boolean; comments_endpoint: string; } @@ -43,6 +46,7 @@ export default function PublicFileShow({ preview_url, download_url, download_limit, + unscanned, comments_enabled, comments_endpoint, }: PublicFileShowProps) { @@ -81,6 +85,8 @@ export default function PublicFileShow({ + + {comments_enabled && (
diff --git a/resources/js/pages/public/themes/drive/file.tsx b/resources/js/pages/public/themes/drive/file.tsx index 946aff06..7a250d88 100644 --- a/resources/js/pages/public/themes/drive/file.tsx +++ b/resources/js/pages/public/themes/drive/file.tsx @@ -2,6 +2,7 @@ import { Head } from '@inertiajs/react'; import { CommentsShellDrive } from '@/components/comments/shells/comments-shell-drive'; import { DownloadAction } from '@/components/download-action'; +import { UnscannedNotice } from '@/components/files/unscanned-notice'; import { PreviewAction } from '@/components/preview-action'; import { CategoryBadges, type CategoryTag } from '@/components/files/category-badges'; import { type VersionLinks } from '@/components/files/version-badge'; @@ -33,6 +34,8 @@ interface PublicFileShowProps { preview_url: string | null; download_url: string; download_limit: DownloadLimit; + /** Whether the file went out unchecked — see UnscannedNotice. */ + unscanned?: boolean; comments_enabled: boolean; comments_endpoint: string; } @@ -43,6 +46,7 @@ export default function PublicFileShowDrive({ preview_url, download_url, download_limit, + unscanned, comments_enabled, comments_endpoint, }: PublicFileShowProps) { @@ -92,6 +96,8 @@ export default function PublicFileShowDrive({ />
+ + {comments_enabled && (
diff --git a/resources/js/pages/public/themes/gallery/file.tsx b/resources/js/pages/public/themes/gallery/file.tsx index a2b7730e..9cfd78b4 100644 --- a/resources/js/pages/public/themes/gallery/file.tsx +++ b/resources/js/pages/public/themes/gallery/file.tsx @@ -3,6 +3,7 @@ import { File as FileIcon } from 'lucide-react'; import { CommentsShellGallery } from '@/components/comments/shells/comments-shell-gallery'; import { DownloadAction } from '@/components/download-action'; +import { UnscannedNotice } from '@/components/files/unscanned-notice'; import { PreviewAction } from '@/components/preview-action'; import { CategoryBadges, type CategoryTag } from '@/components/files/category-badges'; import { type VersionLinks } from '@/components/files/version-badge'; @@ -33,6 +34,8 @@ interface PublicFileShowProps { preview_url: string | null; download_url: string; download_limit: DownloadLimit; + /** Whether the file went out unchecked — see UnscannedNotice. */ + unscanned?: boolean; comments_enabled: boolean; comments_endpoint: string; } @@ -43,6 +46,7 @@ export default function PublicFileShowGallery({ preview_url, download_url, download_limit, + unscanned, comments_enabled, comments_endpoint, }: PublicFileShowProps) { @@ -84,6 +88,8 @@ export default function PublicFileShowGallery({
+ + {comments_enabled && (
diff --git a/resources/js/pages/share/show.tsx b/resources/js/pages/share/show.tsx index c64f2e36..8ab556fa 100644 --- a/resources/js/pages/share/show.tsx +++ b/resources/js/pages/share/show.tsx @@ -2,6 +2,7 @@ import { Head } from '@inertiajs/react'; import { Download } from 'lucide-react'; import { CategoryBadges, type CategoryTag } from '@/components/files/category-badges'; +import { UnscannedNotice } from '@/components/files/unscanned-notice'; import { Button } from '@/components/ui/button'; import { useTranslation } from '@/hooks/use-translation'; import AuthLayout from '@/layouts/auth-layout'; @@ -15,9 +16,11 @@ interface ShareShowProps { categories: CategoryTag[]; }; download_url?: string; + /** Whether the file went out unchecked — see UnscannedNotice. */ + unscanned?: boolean; } -export default function ShareShow({ status, file, download_url }: ShareShowProps) { +export default function ShareShow({ status, file, download_url, unscanned }: ShareShowProps) { const { t } = useTranslation(); if (status !== 'active' || !file || !download_url) { @@ -56,6 +59,8 @@ export default function ShareShow({ status, file, download_url }: ShareShowProps {t('Download')} + + ); } diff --git a/tests/Feature/Files/UnscannedNoticeTest.php b/tests/Feature/Files/UnscannedNoticeTest.php new file mode 100644 index 00000000..6d2d7b02 --- /dev/null +++ b/tests/Feature/Files/UnscannedNoticeTest.php @@ -0,0 +1,109 @@ +admin = User::factory()->create(); + $this->settings = app(Settings::class); + + $this->settings->set(Setting::VirusScanningEnabled, true); + $this->settings->set(Setting::VirusScannerAddress, 'tcp://scanner.test:3310'); + $this->settings->set(Setting::PublicListingEnabled, true); + $this->settings->set(Setting::PublicListingSlug, 'public'); + $this->settings->set(Setting::Theme, 'default'); +}); + +/** The `unscanned` prop on the share page for a file in this state. */ +function sharedFileNotice(array $scan): bool +{ + $file = File::factory()->create(array_merge(['uploaded_by' => test()->admin->id], $scan)); + $link = app(CreateShareLink::class)->for($file, test()->admin); + + $notice = null; + + test()->get("/s/{$link->token}")->assertInertia(function (AssertableInertia $page) use (&$notice) { + $notice = $page->toArray()['props']['unscanned']; + }); + + return $notice; +} + +test('a link to a file nothing checked says so', function () { + expect(sharedFileNotice([ + 'scan_status' => ScanStatus::NotScanned, + 'scan_note' => NotScannedReason::TooLarge->value, + ]))->toBeTrue(); +}); + +test('the same for a file that went out while the scanner was down, or that it could not open', function () { + foreach ([NotScannedReason::ScannerUnavailable, NotScannedReason::Encrypted] as $reason) { + expect(sharedFileNotice(['scan_status' => ScanStatus::NotScanned, 'scan_note' => $reason->value])) + ->toBeTrue($reason->value); + } +}); + +test('a file that passed says nothing', function () { + expect(sharedFileNotice(['scan_status' => ScanStatus::Clean]))->toBeFalse(); +}); + +test('a file from before this installation scanned says nothing', function () { + // Every file on an installation that has only just switched scanning + // on is in this state. Saying it about all of them says nothing about + // any of them. + expect(sharedFileNotice([ + 'scan_status' => ScanStatus::NotScanned, + 'scan_note' => NotScannedReason::BeforeScanning->value, + ]))->toBeFalse(); + + expect(sharedFileNotice(['scan_status' => ScanStatus::NotScanned, 'scan_note' => null]))->toBeFalse(); +}); + +test('an installation that does not scan says nothing about any of it', function () { + $this->settings->set(Setting::VirusScanningEnabled, false); + + expect(sharedFileNotice([ + 'scan_status' => ScanStatus::NotScanned, + 'scan_note' => NotScannedReason::TooLarge->value, + ]))->toBeFalse(); +}); + +test('the public file page says it too, in every theme', function (string $theme) { + $this->settings->set(Setting::Theme, $theme); + + $group = Group::query()->create(['name' => 'Showcase', 'public' => true]); + $file = File::factory()->public()->create([ + 'uploaded_by' => $this->admin->id, + 'scan_status' => ScanStatus::NotScanned, + 'scan_note' => NotScannedReason::ScannerUnavailable->value, + ]); + shareFileWithGroup($file, $group); + + $this->get("/public/files/{$file->slug}")->assertInertia( + fn (AssertableInertia $page) => $page + ->component("public/themes/{$theme}/file") + ->where('unscanned', true), + ); +})->with(['default', 'compact', 'drive', 'gallery']);