Kundenansicht neu P2: Adressen auf derselben Herkunft zulassen (Vorschau, Zahlungsseiten)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 17:24:27 +00:00
1 parent 2ac7d3f64c
commit eb7c44ac11
8 files changed
+26 -14

No files matched your search

+2 -2
View File
@@ -5,7 +5,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, ladeAbo, safeUrl, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
const BEDINGUNGEN = '/abo-bedingungen.html' const BEDINGUNGEN = '/abo-bedingungen.html'
@@ -101,7 +101,7 @@ function bestellung(daten, productKey, viewAs) {
method: 'POST', method: 'POST',
body: { productKey, acceptTerms, startChoice, termsVersion: daten.termsVersion, offerId: offer?.id || '', lateStartAt: daten.lateStartAt || '' }, body: { productKey, acceptTerms, startChoice, termsVersion: daten.termsVersion, offerId: offer?.id || '', lateStartAt: daten.lateStartAt || '' },
}) })
const ziel = safeUrl(url) const ziel = sichereAdresse(url)
if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte später noch einmal versuchen.') if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte später noch einmal versuchen.')
window.location.href = ziel window.location.href = ziel
} catch (f) { } catch (f) {
+7
View File
@@ -181,4 +181,11 @@ export function wechselFunktionAus(sub) {
return { key: f.key, projectId: f.projectId } return { key: f.key, projectId: f.projectId }
} }
/** Adressen aus Daten (previewUrl, portalUrl, paymentUrl): http(s) wie safeUrl, dazu Pfade auf derselben Herkunft ("/vorschau-hinweis.html").
* Nicht zugelassen: javascript:, data:, "//host" und "/\\host" (Browser lesen den Rückstrich wie einen Schrägstrich). Sonst ''. */
export function sichereAdresse(url) {
const text = typeof url === 'string' ? url : ''
return safeUrl(text) || (/^\/(?![/\\])/.test(text) ? text : '')
}
export { safeUrl } export { safeUrl }
+2 -2
View File
@@ -3,7 +3,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, safeUrl } from './ansicht-hilfen.js' import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, sichereAdresse } from './ansicht-hilfen.js'
import { baueAktivitaet, baueProjektDetails, ladeProjektDetails } from './website-details.js' import { baueAktivitaet, baueProjektDetails, ladeProjektDetails } from './website-details.js'
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js' import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js' import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js'
@@ -47,7 +47,7 @@ const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null,
function projektZeile(projekt) { function projektZeile(projekt) {
const bereit = vorschauBereit(projekt) const bereit = vorschauBereit(projekt)
const url = safeUrl(projekt.previewUrl) const url = sichereAdresse(projekt.previewUrl)
const status = projektStatus(projekt) const status = projektStatus(projekt)
const rahmen = bereit && url const rahmen = bereit && url
? h('div', { class: 'kn-vorschau', 'data-vorschau': true }, h('iframe', { src: url, loading: 'lazy', scrolling: 'no', tabindex: '-1', 'aria-hidden': 'true', title: 'Vorschau deiner Website' })) ? h('div', { class: 'kn-vorschau', 'data-vorschau': true }, h('iframe', { src: url, loading: 'lazy', scrolling: 'no', tabindex: '-1', 'aria-hidden': 'true', title: 'Vorschau deiner Website' }))
+2 -2
View File
@@ -5,7 +5,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, ladeAbo, wechselFunktionAus, safeUrl, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
const SCHLUESSEL = 'kn-wechsel-funktion' const SCHLUESSEL = 'kn-wechsel-funktion'
const BEDINGUNGEN = '/abo-bedingungen.html' const BEDINGUNGEN = '/abo-bedingungen.html'
@@ -141,7 +141,7 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
const res = await api('/api/abo/switch', { method: 'POST', body }) const res = await api('/api/abo/switch', { method: 'POST', body })
if (gesendet) vergissFunktion() if (gesendet) vergissFunktion()
if (res.paymentUrl) { if (res.paymentUrl) {
const ziel = safeUrl(res.paymentUrl) const ziel = sichereAdresse(res.paymentUrl)
if (!ziel) { if (!ziel) {
lauf.busy = false lauf.busy = false
zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.') zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.')
+2 -2
View File
@@ -3,7 +3,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h } from './dom.js' import { h } from './dom.js'
import { fmtEuro, safeUrl, zuAnsicht } from './ansicht-hilfen.js' import { fmtEuro, sichereAdresse, zuAnsicht } from './ansicht-hilfen.js'
/** Rechnungen mit offenem Betrag, in der Reihenfolge der Antwort von GET /api/invoices. */ /** Rechnungen mit offenem Betrag, in der Reihenfolge der Antwort von GET /api/invoices. */
export function offeneRechnungen(antwort) { export function offeneRechnungen(antwort) {
@@ -25,7 +25,7 @@ export function rechnungOffenZeile(offen) {
if (!Array.isArray(offen) || !offen.length) return null if (!Array.isArray(offen) || !offen.length) return null
const summe = Math.round(offen.reduce((s, r) => s + (Number(r.balance) || 0), 0) * 100) / 100 const summe = Math.round(offen.reduce((s, r) => s + (Number(r.balance) || 0), 0) * 100) / 100
const einzel = offen.length === 1 ? offen[0] : null const einzel = offen.length === 1 ? offen[0] : null
const url = einzel ? safeUrl(einzel.portalUrl) : '' const url = einzel ? sichereAdresse(einzel.portalUrl) : ''
const link = url const link = url
? h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Zahlen') ? h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Zahlen')
: h('a', zuAnsicht('invoices'), 'Zahlen') : h('a', zuAnsicht('invoices'), 'Zahlen')
+3 -3
View File
@@ -3,7 +3,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, safeUrl } from './ansicht-hilfen.js' import { K, zuAnsicht, fmtEuro, fmtDatum, fmtTagMonat, sichereAdresse } from './ansicht-hilfen.js'
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.' const TEXT_UST = 'Gemäß § 19 UStG ohne Umsatzsteuer.'
@@ -61,7 +61,7 @@ function zahlungsKnopf(viewAs) {
fehler.hidden = true fehler.hidden = true
try { try {
const { url } = await api('/api/abo/portal', { method: 'POST', body: {} }) const { url } = await api('/api/abo/portal', { method: 'POST', body: {} })
const ziel = safeUrl(url) const ziel = sichereAdresse(url)
if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuche es später noch einmal.') if (!ziel) throw new Error('Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuche es später noch einmal.')
window.location.href = ziel window.location.href = ziel
} catch (f) { } catch (f) {
@@ -77,7 +77,7 @@ function wechselKarte(daten, sub) {
if (sub.productKey !== 'hosting') return null if (sub.productKey !== 'hosting') return null
const wartung = produkt(daten, 'wartung') const wartung = produkt(daten, 'wartung')
if (sub.switchPending) { if (sub.switchPending) {
const url = safeUrl(sub.switchPending.paymentUrl) const url = sichereAdresse(sub.switchPending.paymentUrl)
return h('section', { class: 'kn-sektion kn-wechsel' }, return h('section', { class: 'kn-sektion kn-wechsel' },
h('h3', null, 'Dein Wechsel wartet auf die Zahlung'), h('h3', null, 'Dein Wechsel wartet auf die Zahlung'),
h('p', { class: 'kn-hinweis' }, `Dein Wechsel zum Wartungs-Abo ist erst abgeschlossen, wenn die Zahlung bestätigt ist.${url ? '' : ' Schreib uns gern im Chat, wenn du Hilfe brauchst.'}`), h('p', { class: 'kn-hinweis' }, `Dein Wechsel zum Wartungs-Abo ist erst abgeschlossen, wenn die Zahlung bestätigt ist.${url ? '' : ' Schreib uns gern im Chat, wenn du Hilfe brauchst.'}`),
+2 -2
View File
@@ -3,7 +3,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, legacyRequest, safeUrl, fmtEuro, fmtDatum } from './ansicht-hilfen.js' import { K, legacyRequest, sichereAdresse, fmtEuro, fmtDatum } from './ansicht-hilfen.js'
import { initAuftraege } from '../auftrag-portal.js' import { initAuftraege } from '../auftrag-portal.js'
const TEXT_LEER = 'Es liegen noch keine Rechnungen vor.' const TEXT_LEER = 'Es liegen noch keine Rechnungen vor.'
@@ -14,7 +14,7 @@ const pille = (r) => (r.overdue ? K.pille.rot : r.paid ? K.pille.gruen : r.cance
function rechnungsZeile(r) { function rechnungsZeile(r) {
const offen = Number(r.balance) > 0 const offen = Number(r.balance) > 0
const url = safeUrl(r.portalUrl) const url = sichereAdresse(r.portalUrl)
const links = [ const links = [
url ? h('a', { class: 'kn-zeilenlink', href: url, target: '_blank', rel: 'noopener noreferrer' }, offen ? 'Zahlen' : 'Ansehen') : null, url ? h('a', { class: 'kn-zeilenlink', href: url, target: '_blank', rel: 'noopener noreferrer' }, offen ? 'Zahlen' : 'Ansehen') : null,
r.hasPdf !== false ? h('a', { class: 'kn-zeilenlink', href: `/api/invoices/${encodeURIComponent(r.id)}/pdf` }, 'PDF') : null, r.hasPdf !== false ? h('a', { class: 'kn-zeilenlink', href: `/api/invoices/${encodeURIComponent(r.id)}/pdf` }, 'PDF') : null,
+6 -1
View File
@@ -2,7 +2,7 @@ import { test } from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { import {
fmtEuro, fmtDatum, fmtDatumZeit, fmtTagMonat, fmtStd, fmtMinuten, fmtBytes, legacyRequest, meldeZaehlerNeu, merkeHinweis, nimmHinweis, fmtEuro, fmtDatum, fmtDatumZeit, fmtTagMonat, fmtStd, fmtMinuten, fmtBytes, legacyRequest, meldeZaehlerNeu, merkeHinweis, nimmHinweis,
setzeChatVorgabe, nimmChatVorgabe, fmtUhrzeit, berlinTag, ersetze, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl, setzeChatVorgabe, nimmChatVorgabe, fmtUhrzeit, berlinTag, ersetze, sichereAdresse, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl,
} from '../public/neu/ansicht-hilfen.js' } from '../public/neu/ansicht-hilfen.js'
import { mit, mitDom, abo, aboAntwort, me, projekt, rechnung, nachricht, angebot, ledger, anfrageGutschrift, VORSCHAU, VORSCHAU_SPEICHER, LIMITS, neueWurzel } from './support/knDaten.js' import { mit, mitDom, abo, aboAntwort, me, projekt, rechnung, nachricht, angebot, ledger, anfrageGutschrift, VORSCHAU, VORSCHAU_SPEICHER, LIMITS, neueWurzel } from './support/knDaten.js'
@@ -97,3 +97,8 @@ test('mit(): unbekannte Pfade antworten 404, Funktionen liefern Status und Body,
const c = await fetch('/api/z'); assert.equal(c.status, 404); assert.deepEqual(await c.json(), { error: 'unbekannt' }) const c = await fetch('/api/z'); assert.equal(c.status, 404); assert.deepEqual(await c.json(), { error: 'unbekannt' })
const ev = new CustomEvent('x', { detail: 1 }); assert.equal(ev.detail, 1); ev.target = 'frei'; assert.equal(ev.target, 'frei') const ev = new CustomEvent('x', { detail: 1 }); assert.equal(ev.detail, 1); ev.target = 'frei'; assert.equal(ev.target, 'frei')
})) }))
test('sichereAdresse: http(s) und Pfade auf derselben Herkunft, sonst leer', () => {
assert.equal(sichereAdresse('https://a.de/x'), 'https://a.de/x'); assert.equal(sichereAdresse('/vorschau-hinweis.html'), '/vorschau-hinweis.html')
for (const v of ['javascript:alert(1)', 'data:text/html,x', '//evil.example/x', '/\\evil.example/x', 'vorschau.html', '', null, undefined, 5]) assert.equal(sichereAdresse(v), '', String(v))
})