mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-21 19:13:20 +00:00
Tell whoever opens a public link that nothing checked the file
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.
This commit is contained in:
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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.": "このファイルはウイルス検査を受けていません。"
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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.": "Этот файл не проверялся на вирусы."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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ı."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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."
|
||||
}
|
||||
|
||||
+2
-1
@@ -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.": "此文件未经过病毒检查。"
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<p className={cn('text-muted-foreground flex items-center justify-center gap-1.5 text-xs', className)}>
|
||||
<ShieldQuestion className="size-3.5 shrink-0" />
|
||||
{t('This file was not checked for viruses.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
/>
|
||||
<DownloadAction href={download_url} limit={download_limit} size="sm" />
|
||||
</div>
|
||||
|
||||
<UnscannedNotice unscanned={unscanned} className="justify-start" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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({
|
||||
<DownloadAction href={download_url} limit={download_limit} size="default" />
|
||||
</div>
|
||||
|
||||
<UnscannedNotice unscanned={unscanned} />
|
||||
|
||||
{comments_enabled && (
|
||||
<div className="w-full max-w-lg">
|
||||
<CommentsShellDefault fileName={file.name} endpoint={comments_endpoint} inline />
|
||||
|
||||
@@ -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({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UnscannedNotice unscanned={unscanned} />
|
||||
|
||||
{comments_enabled && (
|
||||
<div className="w-full">
|
||||
<CommentsShellDrive fileName={file.name} endpoint={comments_endpoint} inline />
|
||||
|
||||
@@ -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({
|
||||
<DownloadAction href={download_url} limit={download_limit} size="lg" />
|
||||
</div>
|
||||
|
||||
<UnscannedNotice unscanned={unscanned} />
|
||||
|
||||
{comments_enabled && (
|
||||
<div className="w-full">
|
||||
<CommentsShellGallery fileName={file.name} endpoint={comments_endpoint} inline />
|
||||
|
||||
@@ -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
|
||||
<Download className="size-4" /> {t('Download')}
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<UnscannedNotice unscanned={unscanned} className="mt-4" />
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Modules\Files\Models\File;
|
||||
use App\Modules\Files\Scanning\NotScannedReason;
|
||||
use App\Modules\Files\Scanning\ScanStatus;
|
||||
use App\Modules\Files\Sharing\CreateShareLink;
|
||||
use App\Modules\Groups\Models\Group;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
|
||||
/**
|
||||
* What somebody following a public link is told about a file nothing
|
||||
* checked.
|
||||
*
|
||||
* 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. The uploader sees that on their own file and
|
||||
* staff see it in the library. The person holding the link sees the same
|
||||
* page as for a file that passed, and they neither chose the policy nor
|
||||
* can see the setting.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
Storage::fake('files');
|
||||
$this->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']);
|
||||
Reference in New Issue
Block a user