syncova-policies/lib/site-pages.ts
Weapie 0134dd6db4
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
alangu.de fuer die Gebaerdensprach-Einbindung freigeben
Der Player wird als iframe eingebunden, aber nicht als rohes HTML aus dem
Textfeld: Die Anwendung setzt die Attribute, im Admin steht nur die
Adresse. Damit laesst sich ueber das Feld nichts anderes einschleusen.

Freigegeben ist alangu.de samt Unterdomains, sonst nichts. Die Pruefung
haengt am Hostnamen, nicht an einem Textvergleich - getestet mit
boese-alangu.de.angreifer.example und evil.example/?a=alangu.de, beide
werden abgewiesen, ebenso http statt https.

Vom uebergebenen Einbettungscode weggelassen: accelerometer und
gyroscope. Ein Videoplayer braucht keine Bewegungssensoren.

Ausserdem: Die Gebaerdensprach-Seite erscheint jetzt auch ohne Text,
wenn eine Videoadresse hinterlegt ist - dort ist das Video der Inhalt.
Fusszeile und Seite entscheiden das ueber dieselbe Funktion, sonst gaebe
es eine erreichbare Seite ohne Link.

Geprueft am echten Player: laedt mit HTTP 200 samt CSS, JS und hls.js,
axe-core meldet in hell und dunkel keine Verstoesse, der Rahmen ist per
Tastatur erreichbar, und die Fusszeile verlinkt die Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:28:41 +02:00

144 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Die eigenständigen Seiten des Portals.
*
* Drei rechtliche Pflichtseiten sowie zwei Seiten, die § 4 BITV 2.0 von
* öffentlichen Stellen verlangt: Erläuterungen in Leichter Sprache und in
* Deutscher Gebärdensprache.
*/
export const SITE_PAGES = [
"imprint",
"privacy",
"accessibility",
"easyLanguage",
"signLanguage",
] as const
/** Bezeichner einer Seite. */
export type SitePageKey = (typeof SITE_PAGES)[number]
/** Überschrift und URL je Seite – für Fußzeile, Admin und die Seiten selbst. */
export const SITE_PAGE_META: Record<
SitePageKey,
{ title: string; short: string; path: string; hint: string }
> = {
imprint: {
title: "Impressum",
short: "Impressum",
path: "/impressum",
hint: "Anbieterkennzeichnung nach § 5 DDG",
},
privacy: {
title: "Datenschutzerklärung",
short: "Datenschutz",
path: "/datenschutz",
hint: "Datenverarbeitung durch dieses Portal selbst",
},
accessibility: {
title: "Erklärung zur Barrierefreiheit",
short: "Barrierefreiheit",
path: "/barrierefreiheit",
hint: "Pflichtangabe öffentlicher Stellen nach § 7 BITV 2.0, jährlich zu überprüfen",
},
easyLanguage: {
title: "Leichte Sprache",
short: "Leichte Sprache",
path: "/leichte-sprache",
hint: "Pflichtangabe nach § 4 BITV 2.0: wesentliche Inhalte, Navigation und Barrierefreiheitserklärung in Leichter Sprache",
},
signLanguage: {
title: "Gebärdensprache",
short: "Gebärdensprache",
path: "/gebaerdensprache",
hint: "Pflichtangabe nach § 4 BITV 2.0: dieselben Inhalte in Deutscher Gebärdensprache, üblicherweise als Video",
},
}
/**
* Zusätzliche Angaben, die keine eigene Seite sind, sondern zu einer gehören.
*
* Das Gebärdensprach-Video steht bewusst in eigenen Feldern und nicht als
* HTML im Markdown: Der Markdown-Prozessor filtert HTML weg, und ihn dafür zu
* öffnen würde die Angriffsfläche über alle Textfelder ausweiten.
*/
export const SITE_MEDIA = ["signLanguageVideo", "signLanguageCaptions"] as const
export type SiteMediaKey = (typeof SITE_MEDIA)[number]
/** Alle Felder, die API und Admin austauschen. */
export type SitePagesContent = Record<SitePageKey | SiteMediaKey, string>
/** Leere Ausgangsinhalte – solange im Admin nichts gepflegt wurde. */
export const EMPTY_SITE_PAGES: SitePagesContent = {
imprint: "",
privacy: "",
accessibility: "",
easyLanguage: "",
signLanguage: "",
signLanguageVideo: "",
signLanguageCaptions: "",
}
/**
* Anbieter, deren Player als eingebetteter Rahmen zugelassen ist.
*
* Bewusst eine kurze Liste statt einer allgemeinen Freigabe für iframes: Ein
* eingebetteter Rahmen lädt beim Seitenaufruf von einem fremden Server und
* überträgt dabei IP-Adresse und Browserkennung der Besucherinnen und
* Besucher. Das ist bei einem Gebärdensprach-Dienst vertretbar, für den ein
* Auftragsverarbeitungsvertrag besteht – bei einem Videoportal wie YouTube
* wäre es das nicht.
*
* Eingetragen wird die Domain; Unterdomains sind eingeschlossen.
*/
export const EMBED_HOSTS = ["alangu.de"] as const
/**
* Hat eine Seite Inhalt und wird deshalb ausgeliefert und verlinkt?
*
* Auf der Gebärdensprach-Seite ist das Video der eigentliche Inhalt; sie zählt
* daher auch ohne Text als gepflegt. Fußzeile und Seite selbst müssen dieselbe
* Regel verwenden, sonst gibt es eine erreichbare Seite ohne Link oder einen
* Link ins Leere.
*/
export function hasSitePageContent(
pages: SitePagesContent,
key: SitePageKey
): boolean {
if (pages[key].trim()) return true
return key === "signLanguage" && pages.signLanguageVideo.trim().length > 0
}
/** Gehört die Adresse zu einem zugelassenen Einbettungs-Anbieter? */
export function isAllowedEmbedUrl(value: string) {
const wert = value.trim()
if (!wert) return false
try {
const url = new URL(wert)
if (url.protocol !== "https:") return false
return EMBED_HOSTS.some(
(host) => url.hostname === host || url.hostname.endsWith(`.${host}`)
)
} catch {
return false
}
}
/**
* Prüft eine Medienadresse. Zugelassen sind eine Adresse auf dem eigenen
* Server (mit / beginnend) und https. Damit sind `javascript:` und `data:`
* ausgeschlossen, bevor der Wert in ein src-Attribut gelangt.
*/
export function isSafeMediaUrl(value: string) {
const wert = value.trim()
if (!wert) return true
if (wert.startsWith("/") && !wert.startsWith("//")) return true
try {
return new URL(wert).protocol === "https:"
} catch {
return false
}
}