waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src main datenschutz.ts

4,9 KB Rohdatei
app/src/main/datenschutz.ts — 166 Zeilen
1 /**
2 * Lädt die Datenschutzerklärung aus der mitgelieferten Datei.
3 *
4 * ## Warum genau wie die Lizenztexte
5 *
6 * Die Anwendung liefert bereits zwei lange Rechtstexte mit und zeigt sie an —
7 * den EUPL-Volltext und die Übersicht der Fremdkomponenten. Der Weg dorthin
8 * ist erprobt und wird gegen das **gepackte** Paket geprüft
9 * (`e2e/gepackt.spec.ts`, „lädt die Lizenztexte aus resources/“): Datei über
10 * `extraResources` ins Paket, im Hauptprozess über `process.resourcesPath`
11 * gelesen, im Quellbaum aufwärts gesucht. Diese Datei macht dasselbe und
12 * erfindet nichts daneben.
13 *
14 * ## Was bei fehlender Datei geschieht
15 *
16 * Die Anwendung läuft weiter und sagt, was fehlt. Ein leerer Bereich ohne
17 * Begründung wäre die schlechteste Auskunft — der Nutzende hielte die
18 * Erklärung für nicht vorhanden statt für nicht ladbar, und ausgerechnet
19 * beim Datenschutz ist das der falsche Eindruck.
20 *
21 * ## Die Prüfsumme wird hier **nicht** geprüft
22 *
23 * Sie steht in der Datei und wird von `tests/datenschutz.test.ts` gegen
24 * `docs/datenschutz.md` gerechnet — also beim Bauen, nicht beim Starten. Zur
25 * Laufzeit liegt die Quelldatei gar nicht vor: `docs/` wird nicht
26 * mitgeliefert. Eine Prüfung, die im Paket nie etwas zu vergleichen hat,
27 * wäre eine Zusage ohne Deckung.
28 */
29
30 import { existsSync, readFileSync } from 'node:fs';
31 import { join } from 'node:path';
32
33 import { app } from 'electron';
34
35 import type {
36 Datenschutzangaben,
37 Datenschutzblock,
38 Datenschutzerklaerung,
39 } from '../shared/datenschutz';
40 import { sucheAufwaerts } from './katalog';
41
42 const DATEI = 'datenschutz.json';
43 const RELATIV = join('content', DATEI);
44
45 /**
46 * Obergrenze beim Lesen.
47 *
48 * Nachgemessen ist die Datei 80.304 Byte groß. Eine Grenze bei zwei Megabyte
49 * lässt jedes denkbare Wachstum zu und fängt zugleich den Fall ab, dass
50 * jemand versehentlich etwas anderes unter diesen Namen legt.
51 */
52 const HOECHSTGROESSE = 2 * 1024 * 1024;
53
54 let speicher: Datenschutzangaben | null = null;
55
56 function fehlerText(fehler: unknown): string {
57 return fehler instanceof Error ? fehler.message : String(fehler);
58 }
59
60 function pfadSuchen(): string | null {
61 if (app.isPackaged) {
62 const pfad = join(process.resourcesPath, DATEI);
63 return existsSync(pfad) ? pfad : null;
64 }
65
66 for (const start of [app.getAppPath(), __dirname, process.cwd()]) {
67 const treffer = sucheAufwaerts(start, RELATIV);
68 if (treffer !== null) {
69 return treffer;
70 }
71 }
72 return null;
73 }
74
75 /** Prüft die Form so weit, dass die Oberfläche sich darauf verlassen kann. */
76 function istBlock(wert: unknown): wert is Datenschutzblock {
77 if (typeof wert !== 'object' || wert === null) {
78 return false;
79 }
80 const art = (wert as { art?: unknown }).art;
81 return art === 'ueberschrift' || art === 'absatz' || art === 'liste' || art === 'code';
82 }
83
84 /**
85 * Baut die Angaben aus dem Dateiinhalt.
86 *
87 * Getrennt von {@link datenschutzLesen}, damit Tests sie ohne Electron und
88 * ohne Dateisystem prüfen können – dieselbe Aufteilung wie bei
89 * `lizenzenBilden`.
90 */
91 export function datenschutzBilden(roh: string | null, warnung: string | null): Datenschutzangaben {
92 if (roh === null) {
93 return { erklaerung: null, warnung };
94 }
95
96 let gelesen: unknown;
97 try {
98 gelesen = JSON.parse(roh);
99 } catch (fehler) {
100 return {
101 erklaerung: null,
102 warnung: `Die Datenschutzerklärung ist nicht lesbar: ${fehlerText(fehler)}`,
103 };
104 }
105
106 const daten = gelesen as Partial<Datenschutzerklaerung>;
107 const bloecke = Array.isArray(daten.bloecke) ? daten.bloecke.filter(istBlock) : [];
108
109 if (
110 typeof daten.fassung !== 'string' ||
111 typeof daten.stand !== 'string' ||
112 typeof daten.quellpruefsumme !== 'string' ||
113 bloecke.length === 0
114 ) {
115 return {
116 erklaerung: null,
117 warnung:
118 'Die mitgelieferte Datenschutzerklärung ist unvollständig. ' +
119 'Die veröffentlichte Fassung gilt unverändert.',
120 };
121 }
122
123 return {
124 erklaerung: {
125 quellpruefsumme: daten.quellpruefsumme,
126 fassung: daten.fassung,
127 stand: daten.stand,
128 bloecke,
129 },
130 warnung: null,
131 };
132 }
133
134 /** Liest die Erklärung, einmal je Programmlauf. */
135 export function datenschutzLesen(): Datenschutzangaben {
136 if (speicher !== null) {
137 return speicher;
138 }
139
140 const pfad = pfadSuchen();
141 if (pfad === null) {
142 speicher = datenschutzBilden(
143 null,
144 `Die Datenschutzerklärung (${DATEI}) wurde nicht gefunden. ` +
145 'Bitte fordern Sie sie unter der Adresse an, die unter „Rückmeldung und ' +
146 'Kontakt“ steht.',
147 );
148 return speicher;
149 }
150
151 let roh: string | null = null;
152 let warnung: string | null = null;
153 try {
154 roh = readFileSync(pfad, 'utf8').slice(0, HOECHSTGROESSE);
155 } catch (fehler) {
156 warnung = `Die Datei ${DATEI} ist nicht lesbar: ${fehlerText(fehler)}`;
157 }
158
159 speicher = datenschutzBilden(roh, warnung);
160 return speicher;
161 }
162
163 /** Leert den Zwischenspeicher – Gegenstück für Tests. */
164 export function datenschutzZuruecksetzen(): void {
165 speicher = null;
166 }