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']);