waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app src shared unterstuetzung.ts
| 1 | /** |
| 2 | * Das Unterstützungsangebot – eine Bitte, kein Handel. |
| 3 | * |
| 4 | * ## Eine Adresse, und leer heißt lautlos |
| 5 | * |
| 6 | * {@link UNTERSTUETZUNG_URL} ist die **einzige** Stelle, an der die Adresse |
| 7 | * steht. Ist sie leer, lässt die Anwendung das Angebot vollständig weg: keine |
| 8 | * Überschrift, kein Knopf, keine leere Zeile. Ein Knopf, der ins Nichts führt, |
| 9 | * wäre schlimmer als kein Knopf – er sagt zu, dass es dort weitergeht. Wer den |
| 10 | * Dienst wechselt oder das Angebot zurücknehmen will, ändert diese eine |
| 11 | * Zeile. |
| 12 | * |
| 13 | * Gewählt ist Ko-fi. Die Form bleibt geprüft: `https:`, keine Zugangsdaten |
| 14 | * in der Adresse, sonst gilt sie als nicht gesetzt. Ein Wechsel des Dienstes |
| 15 | * ist auch später eine Zeile – die Adresse steht nur hier, und |
| 16 | * `main/sicherheit.ts` liest sie von dieser Stelle. |
| 17 | * |
| 18 | * ## Warum es nichts freischaltet |
| 19 | * |
| 20 | * Die Anwendung soll in den Microsoft Store. Dessen Richtlinien lassen bei |
| 21 | * kostenlosen Anwendungen einen Verweis auf eine Spendenseite zu, verlangen |
| 22 | * aber, dass dahinter **keine digitalen Güter und keine Funktionen verkauft** |
| 23 | * werden. Deshalb schaltet dieser Knopf nichts frei: keinen Pro-Modus, keine |
| 24 | * Zusatzfunktion, keine Werbefreiheit – es gibt hier keine Werbung, die man |
| 25 | * abstellen könnte. Wer nichts gibt, hat dieselbe Anwendung wie alle anderen. |
| 26 | * Das ist nicht bloß eine Store-Auflage, sondern der Grund, warum das Angebot |
| 27 | * überhaupt vertretbar ist: Eine Prüfungsvorbereitung, die einen Teil ihres |
| 28 | * Stoffs hinter eine Zahlung legt, benachteiligt genau die Prüflinge, die |
| 29 | * ohnehin am wenigsten übrig haben. |
| 30 | * |
| 31 | * ## Warum es sich nicht von selbst meldet |
| 32 | * |
| 33 | * `docs/entscheidung-motivation.md` hält fest, warum es hier keinen Streak |
| 34 | * und keinen Tagesziel-Ring gibt: Diese Anwendung erzeugt keinen Druck, um |
| 35 | * Verhalten zu erzeugen. Für eine Spendenbitte gilt dasselbe, nur schärfer – |
| 36 | * sie hat ein Eigeninteresse. Deshalb gibt es **kein** Einblenden nach der |
| 37 | * zehnten Frage, keinen Zähler „schon 300 Fragen gelernt“, keine Erinnerung, |
| 38 | * kein zweites Fragen. Das Angebot steht an einer Stelle, an der ohnehin |
| 39 | * nachsieht, wer wissen will, woher diese Anwendung kommt und was sie kostet: |
| 40 | * in „Über diese Software“, unterhalb der Lizenzangaben – und ausdrücklich |
| 41 | * **nicht** vor dem Rückmeldeweg, der dort zuoberst steht. Wer die Ansicht |
| 42 | * öffnet, weil er auf eine Barriere gestoßen ist, soll nicht zuerst um Geld |
| 43 | * gebeten werden. |
| 44 | * |
| 45 | * ## Warum die Anwendung dabei keine Verbindung öffnet |
| 46 | * |
| 47 | * „Diese Anwendung läuft vollständig offline“ ist eine Zusage, die in „Über |
| 48 | * diese Software“ ausdrücklich steht. Der Knopf bricht sie nicht: Er lädt |
| 49 | * nichts, prüft nichts nach und meldet nichts – er übergibt die Adresse an |
| 50 | * den Hauptprozess, und der reicht sie mit `shell.openExternal` an den |
| 51 | * Standardbrowser weiter. Das Fenster verlässt dabei niemand unangekündigt: |
| 52 | * {@link UNTERSTUETZUNG_KNOPF} sagt es in der Beschriftung (WCAG 3.2). |
| 53 | * |
| 54 | * Was über die Brücke geht, wird trotzdem geprüft – `main/sicherheit.ts` |
| 55 | * vergleicht die gewünschte Adresse mit genau dieser Konstanten und öffnet |
| 56 | * sonst nichts. Eine Adresse mitschicken zu dürfen heißt sonst, auch eine |
| 57 | * andere mitschicken zu dürfen. |
| 58 | */ |
| 59 | |
| 60 | /** |
| 61 | * Adresse der Unterstützungsseite. **Leer = kein Angebot.** |
| 62 | * |
| 63 | * Muss mit `https://` beginnen; alles andere behandelt |
| 64 | * {@link unterstuetzungsziel} wie „nicht gesetzt“. Wird zugleich von |
| 65 | * `main/sicherheit.ts` als einzige zulässige Zieladresse gelesen – eine |
| 66 | * Änderung hier verschiebt also auch, was die Brücke überhaupt öffnen darf. |
| 67 | */ |
| 68 | export const UNTERSTUETZUNG_URL = 'https://ko-fi.com/olafwillerding'; |
| 69 | |
| 70 | /** Überschrift des Blocks in „Über diese Software“. */ |
| 71 | export const UNTERSTUETZUNG_TITEL = 'Diese Arbeit unterstützen'; |
| 72 | |
| 73 | /** |
| 74 | * Beschriftung des Knopfes. |
| 75 | * |
| 76 | * Sie sagt beides: was geschieht und **dass das Fenster verlassen wird**. Ein |
| 77 | * Verweis, der unangekündigt den Browser startet, ist genau die Überraschung, |
| 78 | * die WCAG 3.2 vermeiden will. Die sichtbare Beschriftung ist zugleich der |
| 79 | * zugängliche Name – es gibt kein `aria-label`, das daneben laufen könnte |
| 80 | * (WCAG 2.5.3). |
| 81 | */ |
| 82 | export const UNTERSTUETZUNG_KNOPF = 'Unterstützungsseite im Browser öffnen'; |
| 83 | |
| 84 | /** |
| 85 | * Die geprüfte Adresse, oder `null`, wenn keine gesetzt ist. |
| 86 | * |
| 87 | * Eine reine Funktion mit ausdrücklichem Eingabewert, damit beide Seiten |
| 88 | * dasselbe rechnen und beide Fälle prüfbar sind – auch der, den es heute noch |
| 89 | * nicht gibt. |
| 90 | * |
| 91 | * Abgelehnt wird alles, was kein `https:` ist. `http:` fiele in der |
| 92 | * Auslieferung an unbekannte Empfänger auf; `mailto:`, `javascript:` und |
| 93 | * Ähnliches haben an dieser Stelle nichts verloren. Zugangsdaten in der |
| 94 | * Adresse (`https://name:wort@…`) gelten ebenfalls als Fehleintrag: Sie |
| 95 | * gehören in keinen Verweis, den jemand im Browser sieht. |
| 96 | * |
| 97 | * Ein Vertipper führt damit zu demselben Ergebnis wie ein leeres Feld – das |
| 98 | * Angebot entfällt. Eine Fehlermeldung an dieser Stelle wäre für den |
| 99 | * Lernenden ein Mangel der Anwendung, obwohl es einer des Eintrags ist. |
| 100 | */ |
| 101 | export function unterstuetzungsziel(url: string): string | null { |
| 102 | const adresse = url.trim(); |
| 103 | if (adresse.length === 0) { |
| 104 | return null; |
| 105 | } |
| 106 | |
| 107 | let geprueft: URL; |
| 108 | try { |
| 109 | geprueft = new URL(adresse); |
| 110 | } catch { |
| 111 | return null; |
| 112 | } |
| 113 | |
| 114 | if (geprueft.protocol !== 'https:') { |
| 115 | return null; |
| 116 | } |
| 117 | |
| 118 | /* Hier stand zusätzlich eine Prüfung auf einen leeren Rechnernamen. Sie ist |
| 119 | entfernt, weil sie nicht auslösen kann: Für die besonderen Schemata – |
| 120 | `https:` gehört dazu – verlangt der URL-Standard einen Rechnernamen. |
| 121 | Nachgemessen mit Node: `https://` wirft `ERR_INVALID_URL` (dann greift der |
| 122 | `catch` oben), und `https:///pfad` liefert `pfad` als Rechnernamen. Eine |
| 123 | Bedingung, die nie zutrifft, behauptet einen Schutz, den es nicht gibt – |
| 124 | dieselbe Überlegung wie bei den stummen Wachen in `sicherheit.test.ts`. */ |
| 125 | |
| 126 | if (geprueft.username.length > 0 || geprueft.password.length > 0) { |
| 127 | return null; |
| 128 | } |
| 129 | |
| 130 | /* Zurück kommt die eingetragene Schreibweise und nicht `geprueft.href`: |
| 131 | Angezeigt und geöffnet wird so dieselbe Zeichenkette, und der Vergleich |
| 132 | im Hauptprozess trifft genau das, was auf dem Bildschirm stand. */ |
| 133 | return adresse; |
| 134 | } |