From 6c10c9bbe0337402dce40a9ecc5e841a7df44b6c Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 21 Sep 2026 15:02:08 +0200 Subject: [PATCH] feat(quick-260921-jt4): uebersetzter ZIP-Name im Zertifikat-Aufteiler mit Windows-Schutzfunktion (Restposten 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit downloadAllAsZip liegt ausserhalb der Komponente und kann den Uebersetzungs-Hook nicht aufrufen; der Name kommt jetzt als Parameter herein, Aufrufstelle uebergibt t('actions.zipFilename'). Neuer Schluessel unter certManager.actions: deutsch "Zertifikate.zip", englisch "certificates.zip". Der 260921-bi2-Einwand (ein uebersetzter Name koenne Umlaute auf eine Windows-Freigabe tragen) trifft fuer diesen konkreten Text nicht zu — das deutsche Wort enthaelt keinen Umlaut. Die Sicherheit haengt darauf aber NICHT: neue Datei zip-filename.ts mit einer fuer sich pruefbaren Schutzfunktion, die Windows-verbotene Zeichen, Steuerzeichen und Nicht-ASCII ersetzt, abschliessende Punkte/Leerzeichen entfernt, reservierte Geraetenamen abfaengt, bei leerem Ergebnis auf certificates.zip zurueckfaellt und die .zip-Endung sicherstellt. SplitTab.tsx schickt den uebersetzten Namen durch diese Funktion, bevor er am Download landet. Die Dateinamen IM Archiv bleiben unangetastet. zip-filename.test.ts deckt beide Katalogwerte (unveraendert), Umlaut, verbotenes Zeichen, Steuerzeichen, abschliessende Punkte/Leerzeichen, fehlende/vorhandene Endung, leeres Ergebnis und reservierte Geraetenamen ab. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J --- .../cert-manager/components/SplitTab.tsx | 15 ++- .../modules/cert-manager/zip-filename.test.ts | 61 ++++++++++++ .../modules/cert-manager/zip-filename.ts | 93 +++++++++++++++++++ apps/web/src/messages/de.json | 3 +- apps/web/src/messages/en.json | 3 +- 5 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/(portal)/modules/cert-manager/zip-filename.test.ts create mode 100644 apps/web/src/app/(portal)/modules/cert-manager/zip-filename.ts diff --git a/apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx b/apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx index 0d3e9b3..16ccce9 100644 --- a/apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx +++ b/apps/web/src/app/(portal)/modules/cert-manager/components/SplitTab.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { useTranslations } from 'next-intl'; import { zipSync } from 'fflate'; import { type SplitResponse, type CertRole, splitCertsAction, downloadBase64 } from '../actions'; +import { sanitizeZipFilename } from '../zip-filename'; interface SplitTabProps { file: File | null; @@ -17,7 +18,11 @@ const ROLE_STYLES: Record = { 'end-entity': 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', }; -function downloadAllAsZip(certs: SplitResponse['certs']) { +// `downloadAllAsZip` liegt ausserhalb der Komponente und kann den +// Uebersetzungs-Hook nicht selbst aufrufen — der uebersetzte Name kommt +// deshalb als Parameter herein (Restposten 1, quick-260921-jt4). Die +// einzelnen Dateinamen IM Archiv stammen weiterhin unveraendert aus der API. +function downloadAllAsZip(certs: SplitResponse['certs'], zipFilename: string) { const files: Record = {}; for (const cert of certs) { const bytes = Uint8Array.from(atob(cert.content), (c) => c.charCodeAt(0)); @@ -35,7 +40,11 @@ function downloadAllAsZip(certs: SplitResponse['certs']) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'certificates.zip'; + // T-JT4-05: der uebersetzte Name kann Zeichen tragen, die Windows + // verbietet (Umlaute, Sonderzeichen) — sanitizeZipFilename schneidet ihn + // auf das fuer eine Windows-Freigabe Zulaessige zurueck, unabhaengig davon, + // ob der aktuelle Katalogwert zufaellig schon harmlos ist. + a.download = sanitizeZipFilename(zipFilename); a.click(); URL.revokeObjectURL(url); } @@ -81,7 +90,7 @@ export function SplitTab({ file, pemText: _pemText, password: _password }: Split {result && result.certs.length > 1 && (