feat(demo): a privacy notice, and two fixes found by using the demo

**The privacy notice gets its own page and its own link.** A section inside the
Impressum is not one — it has to be reachable in its own right — and it is
required even though this instance asks for nothing. A web server writes the
caller's IP address into a log on every request and an IP address is personal
data, so "we collect almost nothing" is a statement that has to be made rather
than a reason to omit the page.

Written from what the software does rather than from a template. The cookies
named are the three it actually sets, observed on a real visit; the claim that
nothing reaches a third party is the content security policy, which permits
connections to this server only; and the retention period is read off
APP_DEMO_TTL through DemoMode::ttlDescription() rather than typed into the
prose, because a notice stating a period the reaper does not honour is worse
than one saying nothing, and two copies drift the first time somebody tunes the
setting.

APP_DEMO_PRIVACY_HOST names whoever runs the machine, who is a processor.
Unset, the page says so in red — the same rule the Impressum follows, for the
same reason: a legal notice naming nobody looks compliant from a distance.

**Pressing Receive twice looked broken.** The second press appeared to do
nothing for up to thirteen seconds. _refreshList() coalesces bursts through a
fifteen-second floor measured from the LAST refresh, so the press right after a
delivery was the one certain to be inside it. The floor is right — a sync
publishes one event per mailbox per account, and it turns eight fetches into
one — but it was being applied to somebody who had just pressed a button and was
watching for the result. refreshNow() skips it; onAccountSynced() still does
not. The difference is whether anybody is waiting.

**The user menu still wore its own tooltip.** The previous fix stopped a NEW
bubble appearing over an open menu but left the one already on screen, which is
why it vanished only when the pointer left. There is a document-level click
listener whose whole job is to take it down, and ui--user-menu#toggle calls
stopPropagation(), so the click never reached it. The listener runs in the
CAPTURE phase now — at the document, before the event reaches its target, where
no handler can hide a click from it. The same reason the scroll listener above
it already uses capture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5 committed 2026-08-25 12:44:35 +02:00
1 parent d51c3c8475
commit 748e1cdcdf
19 files changed
+438 -4

No files matched your search

+3
View File
@@ -135,6 +135,9 @@ APP_DEMO_TTL=PT2H
APP_DEMO_IMPRESSUM_NAME=
APP_DEMO_IMPRESSUM_ADDRESS=
APP_DEMO_IMPRESSUM_EMAIL=
# Named in the privacy notice as the processor running the server. Blank shows
# a warning on the page rather than a guess — see APP_DEMO_IMPRESSUM_NAME.
APP_DEMO_PRIVACY_HOST=
###< app/demo ###
###> symfony/mercure-bundle ###
@@ -448,6 +448,30 @@ export default class extends Controller {
this._refreshList();
}
/**
* Somebody pressed a button and is watching the list for the result.
*
* Skips the coalescing window, which is there for the opposite situation: a
* sync run publishes one event per mailbox per account, so "a sync
* happened" arrives as a burst and MIN_REFRESH_MS turns eight fetches into
* one. A person pressing Receive is not a burst — and because the window is
* measured from the LAST refresh, the press right after a delivery was the
* one most likely to be inside it. The mail landed instantly the first
* time and appeared to do nothing for the next thirteen seconds the second,
* which reads as the button breaking after one use.
*
* Deliberately not folded into onAccountSynced(): that one is the server
* volunteering that something changed, and it should stay coalesced. The
* difference between the two is whether anybody is waiting.
*/
refreshNow() {
if (false === this.hasListTarget) {
return;
}
this._refreshList({ immediate: true });
}
_affectsCurrentView(data) {
if (!this.hasListTarget) {
return false;
+10 -2
View File
@@ -90,7 +90,15 @@ export default class extends Controller {
this._onDismiss = () => this._hide();
window.addEventListener("scroll", this._onDismiss, true);
window.addEventListener("resize", this._onDismiss);
document.addEventListener("click", this._onDismiss);
// CAPTURE, like the scroll listener above and for a sharper reason: a
// handler on the thing being clicked can call stopPropagation() and
// this never hears the click at all. ui--user-menu#toggle does exactly
// that, so opening the user menu left its own hint painted across the
// menu until the pointer happened to leave the button — the bubble was
// refused a re-show (see _opensAVisibleMenu) but nothing took down the
// one already on screen. Capture runs at the document BEFORE the event
// reaches its target, so no handler can hide a click from this.
document.addEventListener("click", this._onDismiss, true);
document.addEventListener("turbo:before-render", this._onDismiss);
// A trigger can leave the page while the pointer is still on it, and
@@ -127,7 +135,7 @@ export default class extends Controller {
document.removeEventListener("keydown", this._onKeydown);
window.removeEventListener("scroll", this._onDismiss, true);
window.removeEventListener("resize", this._onDismiss);
document.removeEventListener("click", this._onDismiss);
document.removeEventListener("click", this._onDismiss, true);
document.removeEventListener("turbo:before-render", this._onDismiss);
this._observer?.disconnect();
+1
View File
@@ -47,6 +47,7 @@ x-demo-env: &demo-env
APP_DEMO_IMPRESSUM_NAME: ${APP_DEMO_IMPRESSUM_NAME:-}
APP_DEMO_IMPRESSUM_ADDRESS: ${APP_DEMO_IMPRESSUM_ADDRESS:-}
APP_DEMO_IMPRESSUM_EMAIL: ${APP_DEMO_IMPRESSUM_EMAIL:-}
APP_DEMO_PRIVACY_HOST: ${APP_DEMO_PRIVACY_HOST:-}
services:
# Serves /demo, provisions visitors and renders the demo bar.
+3
View File
@@ -96,6 +96,9 @@ security:
# who has not signed in is exactly who it is for. The route 404s itself
# when demo mode is off, so this opens nothing on a normal install.
- { path: ^/impressum$, roles: PUBLIC_ACCESS }
# The privacy notice, anonymous for the same reason and with more force:
# the reader has no account here and is deciding whether to get one.
- { path: ^/datenschutz$, roles: PUBLIC_ACCESS }
# The tab icon. Browsers fetch it for the login page too — before any
# session exists — and an icon behind the login is a redirect the tab
# strip renders as a missing favicon. Anonymous callers get the
+1
View File
@@ -34,6 +34,7 @@ parameters:
app.demo_impressum_name: '%env(default:app.demo_impressum_blank:APP_DEMO_IMPRESSUM_NAME)%'
app.demo_impressum_address: '%env(default:app.demo_impressum_blank:APP_DEMO_IMPRESSUM_ADDRESS)%'
app.demo_impressum_email: '%env(default:app.demo_impressum_blank:APP_DEMO_IMPRESSUM_EMAIL)%'
app.demo_privacy_host: '%env(default:app.demo_impressum_blank:APP_DEMO_PRIVACY_HOST)%'
services:
# default configuration for services in *this* file
+2 -1
View File
@@ -1,4 +1,4 @@
<!-- translated-from: install/configuration.md sha1:3cfadc2b2626e9f3017f6fab0d8d287184aeb6d9 -->
<!-- translated-from: install/configuration.md sha1:2f2698fae7601d3f4a55f7f252d9174b572ddd01 -->
# Konfigurationsreferenz
Jede Umgebungsvariable, die plMail liest, was sie bewirkt, welchen Vorgabewert sie hat und was
@@ -58,6 +58,7 @@ Das sind die Variablen aus `.env`, in der Reihenfolge, in der sie dort stehen.
| `APP_DEMO_IMPRESSUM_NAME` | Betreiber, der im Impressum der Demo (`/impressum`) genannt wird. | leer | Für eine öffentliche Demo in Deutschland: ja | Leer, und die Seite zeigt statt eines Namens eine sichtbare Warnung. Das ist Absicht: § 5 TMG verlangt einen echten Betreiber, und ein Impressum, das niemanden nennt, sieht von weitem korrekt aus, ohne es zu sein. Wird nur gelesen, wenn `APP_DEMO_MODE` an ist. |
| `APP_DEMO_IMPRESSUM_ADDRESS` | Postanschrift auf derselben Seite. Zeilenumbrüche bleiben erhalten. | leer | Wie oben | Wie oben. |
| `APP_DEMO_IMPRESSUM_EMAIL` | Kontaktadresse auf derselben Seite, als `mailto:`-Link. | leer | Wie oben | Wie oben. |
| `APP_DEMO_PRIVACY_HOST` | Das Unternehmen, das den Server betreibt; wird in der Datenschutzerklärung der Demo (`/datenschutz`) als Auftragsverarbeiter genannt. | leer | Für eine öffentliche Demo: ja | Leer, und die Seite zeigt statt eines Namens eine sichtbare Warnung. Wer die Maschine betreibt, verarbeitet in deinem Auftrag die IP-Adressen der Besucher und muss genannt werden; prüfe, ob ein Auftragsverarbeitungsvertrag besteht — die Seite sagt, dass es einen gibt. Wird nur gelesen, wenn `APP_DEMO_MODE` an ist. |
| `MERCURE_URL` | Die Hub-Adresse, an die die **Anwendung** innerhalb des Docker-Netzes veröffentlicht. | `http://mercure/.well-known/mercure` | Ja | Falsch, und nichts wird veröffentlicht: keine Live-Aktualisierungen, kein sichtbarer Fehler auf der Seite. |
| `MERCURE_PUBLIC_URL` | Die Hub-Adresse, die der **Browser** abonniert. | `https://localhost/.well-known/mercure`, sowohl in `.env` als auch in `compose.yaml` | Ja | Falsch, und der Browser öffnet einen Stream dorthin, wo er nicht hinkommt — Maillisten hören auf, sich selbst zu aktualisieren, während der Rest der Anwendung funktioniert. Wird nur dann aus `APP_PUBLIC_URL` abgeleitet, wenn sie nicht gesetzt oder leer ist, was die Standard-`compose.yaml` verhindert. Siehe [Hinter einem Reverse-Proxy](reverse-proxy.md). |
| `MERCURE_JWT_SECRET` | Signiert die Publisher- und Subscriber-JWTs. | leer — 32 zufällige Bytes, hexadezimal, beim ersten Start erzeugt | Ja, aber erzeugt | Anwendung und Hub müssen denselben Wert halten. Weichen sie voneinander ab, weist der Hub jeden Subscriber ab — aus Sicht des Browsers lautlos. |
+1
View File
@@ -54,6 +54,7 @@ These are the variables in `.env`, in the order they appear there.
| `APP_DEMO_IMPRESSUM_NAME` | Operator named on the demo's legal notice (`/impressum`). | blank | For a public demo in Germany, yes | Blank and the page renders a visible warning instead of a name. That is deliberate: § 5 TMG requires a real operator, and a notice naming nobody looks compliant from a distance without being so. Read only when `APP_DEMO_MODE` is on. |
| `APP_DEMO_IMPRESSUM_ADDRESS` | Postal address on the same page. Newlines are preserved. | blank | As above | As above. |
| `APP_DEMO_IMPRESSUM_EMAIL` | Contact address on the same page, rendered as a `mailto:` link. | blank | As above | As above. |
| `APP_DEMO_PRIVACY_HOST` | The company operating the server, named in the demo's privacy notice (`/datenschutz`) as a processor. | blank | For a public demo, yes | Blank and the page renders a visible warning instead of a name. Whoever runs the machine processes visitors' IP addresses on your behalf and has to be named; check you have a data processing agreement with them, because the notice says you do. Read only when `APP_DEMO_MODE` is on. |
| `MERCURE_URL` | The hub address the **app** publishes to, inside the Docker network. | `http://mercure/.well-known/mercure` | Yes | Wrong and nothing publishes: no live updates, no visible error on the page. |
| `MERCURE_PUBLIC_URL` | The hub address the **browser** subscribes to. | `https://localhost/.well-known/mercure` in both `.env` and `compose.yaml` | Yes | Wrong and the browser opens a stream to somewhere it cannot reach — mail lists stop updating by themselves while the rest of the app works. Derived from `APP_PUBLIC_URL` only when this is unset or empty, which the stock `compose.yaml` prevents. See [Behind a reverse proxy](reverse-proxy.md). |
| `MERCURE_JWT_SECRET` | Signs the publisher and subscriber JWTs. | blank — 32 random bytes, hex, generated on first start | Yes, but generated | The app and the hub must hold the same value. They disagree and the hub rejects every subscriber, silently, from the browser's point of view. |
+29
View File
@@ -108,6 +108,35 @@ Each visitor costs one user, one account, ten threads and ten contacts. Provisio
limited to 30 an hour per address, which is set for a room sharing one NAT address — an office, a
conference hall, a classroom — rather than for one visitor, who needs exactly one.
## The legal pages
A publicly hosted demo in Germany needs an Impressum (§ 5 TMG) and a privacy
notice, and the second is the one people skip: it is required even though this
instance asks for nothing, because a web server writes the caller's IP address
into a log and an IP address is personal data.
Both are served only in demo mode, both without a session, and both are linked
from the login page:
| Page | Needs |
|---|---|
| `/impressum` | `APP_DEMO_IMPRESSUM_NAME`, `APP_DEMO_IMPRESSUM_ADDRESS`, `APP_DEMO_IMPRESSUM_EMAIL` |
| `/datenschutz` | the three above, plus `APP_DEMO_PRIVACY_HOST` |
Unset, each renders a visible warning naming the missing variable rather than a
blank heading — a legal notice naming nobody looks compliant from a distance
and is not.
The retention period in the privacy notice is read from `APP_DEMO_TTL` rather
than written into the text, so tuning the one cannot leave the other claiming
something the reaper does not do.
Two things the software cannot know and you must check: that you hold a data
processing agreement with whoever runs the machine, and that the notice matches
what you actually run. It describes plMail's own behaviour accurately — the
three cookies it sets, and a content security policy that permits no third-party
connections — but it is a description, not legal advice.
## Things that bite
**`APP_DEMO_MODE` is instance-wide, not per-user.** There is no way to run a demo mailbox beside
+32
View File
@@ -54,6 +54,8 @@ final class DemoController extends AbstractController
private readonly string $impressumAddress,
#[Autowire('%app.demo_impressum_email%')]
private readonly string $impressumEmail,
#[Autowire('%app.demo_privacy_host%')]
private readonly string $privacyHost,
) {
}
@@ -181,6 +183,36 @@ final class DemoController extends AbstractController
]);
}
/**
* The privacy notice, on its own page and its own link.
*
* A section inside the Impressum is not one. The GDPR wants this
* "leicht zugänglich" in its own right, and — the part that is easy to get
* wrong — it is required even when almost nothing is processed. A server
* that answers a request writes the caller's IP address into a log, and an
* IP address is personal data; "we collect nothing" is a statement that
* still has to be made, not a reason to omit the page.
*
* Public for the same reason the Impressum is: the reader has no account
* here and is deciding whether to get one.
*
* The operator and the host come from the environment, blank by default and
* saying so, because neither can be guessed.
*/
#[Route('/datenschutz', name: 'app_demo_privacy', methods: ['GET'])]
public function privacy(): Response
{
$this->assertDemoMode();
return $this->render('demo/privacy.html.twig', [
'operatorName' => $this->impressumName,
'operatorAddress' => $this->impressumAddress,
'operatorEmail' => $this->impressumEmail,
'host' => $this->privacyHost,
'ttl' => $this->demoMode->ttlDescription(),
]);
}
private function assertDemoMode(): void
{
if (false === $this->demoMode->isEnabled()) {
+23
View File
@@ -71,6 +71,29 @@ final readonly class DemoMode
}
}
/**
* The retention period in words, for the privacy notice.
*
* Derived from the configured TTL rather than written into the page: a
* privacy notice that states a retention period the software does not
* honour is worse than one that says nothing, and these two would drift the
* first time somebody tuned APP_DEMO_TTL.
*
* Rounded to hours because that is the unit the setting is chosen in and
* the granularity a notice needs. Anything under an hour reads as minutes.
*/
public function ttlDescription(): string
{
$now = new DateTimeImmutable('@0');
$seconds = $this->expiryFrom($now)->getTimestamp();
if ($seconds < 3600) {
return sprintf('%d min', (int) round($seconds / 60));
}
return sprintf('%d h', (int) round($seconds / 3600));
}
/**
* Whether an address belongs to a demo-provisioned user.
*
+5
View File
@@ -34,8 +34,13 @@
moving. That is the whole thing the button exists to demonstrate, and
the reload was hiding it.
#}
{# turbo:submit-end, so the list is re-read the moment the delivery is
acknowledged rather than whenever the coalescing window next opens.
Mercure announces the mail too, but that path is throttled on purpose —
see mail--mail-pane#refreshNow for why the two are kept apart. #}
<form method="post"
action="{{ path('app_demo_receive') }}"
data-action="turbo:submit-end->mail--mail-pane#refreshNow"
class="ml-auto flex items-center gap-3 shrink-0">
<input type="hidden" name="_token" value="{{ csrf_token('demo_receive') }}">
+6
View File
@@ -68,6 +68,12 @@
<section class="space-y-2">
<h2 class="text-sm font-semibold text-ink">{{ 'demo.impressum.privacy_title'|trans }}</h2>
<p class="text-sm text-ink-muted leading-relaxed">{{ 'demo.impressum.privacy'|trans }}</p>
{# The summary stays; the notice itself is its own page. #}
<p class="text-sm">
<a href="{{ path('app_demo_privacy') }}" class="text-accent hover:underline">
{{ 'demo.privacy.title'|trans }}
</a>
</p>
</section>
<section class="space-y-2">
+123
View File
@@ -0,0 +1,123 @@
{% extends '_layout/app.html.twig' %}
{% block title %}{{ 'demo.privacy.title'|trans }} — {{ 'app.name'|trans }}{% endblock %}
{#
The privacy notice for a publicly hosted demo.
Its own page and its own link, because a section inside the Impressum is
not a privacy notice: the GDPR wants this reachable in its own right.
It exists even though this instance asks for nothing. A server that answers
a request writes the caller's IP into a log, and an IP is personal data — so
"we collect almost nothing" is a statement that has to be made rather than a
reason to skip the page. Everything below describes what the software
actually does, and the retention period is read off the configured TTL
rather than typed here, so the two cannot drift.
#}
{% block body %}
<div class="min-h-screen app-bg py-12 px-4">
<div class="w-full max-w-2xl mx-auto">
<a href="{{ path('app_login') }}"
class="inline-flex items-center gap-2 text-sm text-ink-muted hover:text-ink transition-colors mb-6">
<i class="fa-solid fa-arrow-left text-xs" aria-hidden="true"></i>
{{ 'app.name'|trans }}
</a>
<div class="bg-surface rounded-pane shadow-sm border border-line p-8 space-y-8">
<h1 class="text-xl font-semibold text-ink">{{ 'demo.privacy.title'|trans }}</h1>
{% if operatorName is empty and operatorAddress is empty and operatorEmail is empty %}
<div class="rounded-lg alert-danger text-danger text-sm px-4 py-3">
<strong>APP_DEMO_IMPRESSUM_NAME</strong>,
<strong>APP_DEMO_IMPRESSUM_ADDRESS</strong> and
<strong>APP_DEMO_IMPRESSUM_EMAIL</strong> are not set. A privacy notice must
name the controller — set them before making this instance public.
</div>
{% endif %}
{# ── Who ────────────────────────────────────────────────── #}
<section class="space-y-2">
<h2 class="text-sm font-semibold text-ink">{{ 'demo.privacy.controller_title'|trans }}</h2>
<p class="text-sm text-ink-muted leading-relaxed">
{{ 'demo.privacy.controller_intro'|trans }}
</p>
<dl class="text-sm text-ink-soft space-y-1">
{% if operatorName is not empty %}<dd class="text-ink">{{ operatorName }}</dd>{% endif %}
{% if operatorAddress is not empty %}<dd class="whitespace-pre-line">{{ operatorAddress }}</dd>{% endif %}
{% if operatorEmail is not empty %}
<dd><a href="mailto:{{ operatorEmail }}" class="text-accent hover:underline">{{ operatorEmail }}</a></dd>
{% endif %}
</dl>
</section>
{# ── What, and on what basis ────────────────────────────── #}
<section class="space-y-3">
<h2 class="text-sm font-semibold text-ink">{{ 'demo.privacy.processing_title'|trans }}</h2>
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-ink-faint">
{{ 'demo.privacy.logs_title'|trans }}
</h3>
<p class="mt-1 text-sm text-ink-muted leading-relaxed">{{ 'demo.privacy.logs'|trans }}</p>
</div>
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-ink-faint">
{{ 'demo.privacy.account_title'|trans }}
</h3>
{# The retention period comes from DemoMode::ttlDescription(), which
reads the same setting the reaper acts on. #}
<p class="mt-1 text-sm text-ink-muted leading-relaxed">
{{ 'demo.privacy.account'|trans({ '%ttl%': ttl }) }}
</p>
</div>
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-ink-faint">
{{ 'demo.privacy.cookies_title'|trans }}
</h3>
<p class="mt-1 text-sm text-ink-muted leading-relaxed">{{ 'demo.privacy.cookies'|trans }}</p>
<ul class="mt-2 space-y-1 text-sm text-ink-soft">
<li><code class="text-xs">PHPSESSID</code> — {{ 'demo.privacy.cookie_session'|trans }}</li>
<li><code class="text-xs">plmail_ui</code> — {{ 'demo.privacy.cookie_ui'|trans }}</li>
<li><code class="text-xs">mercureAuthorization</code> — {{ 'demo.privacy.cookie_mercure'|trans }}</li>
</ul>
</div>
</section>
{# ── Who else sees it ───────────────────────────────────── #}
<section class="space-y-2">
<h2 class="text-sm font-semibold text-ink">{{ 'demo.privacy.third_parties_title'|trans }}</h2>
<p class="text-sm text-ink-muted leading-relaxed">{{ 'demo.privacy.third_parties'|trans }}</p>
{% if host is not empty %}
<p class="text-sm text-ink-muted leading-relaxed">
{{ 'demo.privacy.host'|trans({ '%host%': host }) }}
</p>
{% else %}
<div class="rounded-lg alert-danger text-danger text-sm px-4 py-3">
<strong>APP_DEMO_PRIVACY_HOST</strong> is not set. The company running the
server is a processor and has to be named here.
</div>
{% endif %}
</section>
{# ── Rights ─────────────────────────────────────────────── #}
<section class="space-y-2">
<h2 class="text-sm font-semibold text-ink">{{ 'demo.privacy.rights_title'|trans }}</h2>
<p class="text-sm text-ink-muted leading-relaxed">{{ 'demo.privacy.rights'|trans }}</p>
<p class="text-sm text-ink-muted leading-relaxed">{{ 'demo.privacy.complaint'|trans }}</p>
</section>
<p class="text-xs text-ink-faint border-t border-line pt-4">
<a href="{{ path('app_demo_impressum') }}" class="text-accent hover:underline">
{{ 'demo.impressum.link'|trans }}
</a>
</p>
</div>
</div>
</div>
{% endblock %}
+7 -1
View File
@@ -131,10 +131,16 @@
instance needs one — and it sits outside the card, below
everything, which is where a visitor looks for it. #}
{% if demo.isEnabled %}
<p class="text-center text-xs text-ink-faint mt-6">
{# Two links, not one with the privacy notice tucked inside the
Impressum: the GDPR wants it reachable in its own right. #}
<p class="text-center text-xs text-ink-faint mt-6 flex items-center justify-center gap-3">
<a href="{{ path('app_demo_impressum') }}" class="hover:text-ink transition-colors">
{{ 'demo.impressum.link'|trans }}
</a>
<span aria-hidden="true">·</span>
<a href="{{ path('app_demo_privacy') }}" class="hover:text-ink transition-colors">
{{ 'demo.privacy.title'|trans }}
</a>
</p>
{% endif %}
+45
View File
@@ -451,6 +451,51 @@ final class DemoFlowTest extends WebTestCase
$this->eraseDemoUsers();
}
// ------------------------------------------------------------- the legal pages
/**
* Both notices are reachable without a session, and they are two pages.
*
* A privacy notice folded into the Impressum is not one — it has to be
* reachable in its own right — and both are read by people who have no
* account here and are deciding whether to get one.
*/
public function testTheLegalPagesAreReadableWithoutSigningIn(): void
{
$client = $this->demoClient();
foreach (['/impressum', '/datenschutz'] as $path) {
$client->request('GET', $path);
self::assertResponseIsSuccessful(sprintf('%s must be readable anonymously', $path));
}
}
public function testTheLoginPageLinksToBothOfThem(): void
{
$client = $this->demoClient();
$crawler = $client->request('GET', '/login');
self::assertGreaterThan(0, $crawler->filter('a[href="/impressum"]')->count());
self::assertGreaterThan(
0,
$crawler->filter('a[href="/datenschutz"]')->count(),
'the privacy notice needs its own link, not a mention inside the Impressum',
);
}
/** Neither exists on an install that is not a demo. */
public function testTheLegalPagesDoNotExistOnANormalInstall(): void
{
$client = static::createClient();
foreach (['/impressum', '/datenschutz'] as $path) {
$client->request('GET', $path);
self::assertResponseStatusCodeSame(404, $path.' should not exist off-demo');
}
}
// ------------------------------------------------------------ the install page
/**
+43
View File
@@ -1316,6 +1316,49 @@ demo:
blurb: Sieh dich in plMail um — mit einem eigenen Postfach, ohne Anmeldung und ohne Adresse.
start: Demo starten
disclaimer: Dein Demo-Postfach wird nach ein paar Stunden automatisch gelöscht.
privacy:
account: >-
Beim Start der Demo wird ein Wegwerf-Konto mit einer zufällig erzeugten
Adresse angelegt. Du wirst nicht nach Namen, E-Mail-Adresse oder Passwort
gefragt, und es wird nichts davon gespeichert. Alles, was du dort eingibst —
Nachrichten, Entwürfe, Kalendereinträge — existiert nur in diesem Konto und
wird zusammen mit ihm automatisch gelöscht, etwa %ttl% nach dem Anlegen. In
der Demo geschriebene Mail wird nie versendet: Es besteht keine Verbindung zu
einem Mailserver.
account_title: Das Demo-Konto
complaint: >-
Außerdem steht dir ein Beschwerderecht bei einer Datenschutz-Aufsichtsbehörde
zu.
controller_intro: Verantwortlich für die hier beschriebene Verarbeitung:
controller_title: Verantwortlicher
cookie_mercure: hält die Verbindung für Live-Aktualisierungen offen, solange du angemeldet bist.
cookie_session: hält dich in deinem Demo-Konto angemeldet. Wird beim Schließen des Browsers gelöscht.
cookie_ui: merkt sich das gewählte Design und die Sprache.
cookies: >-
Es werden drei Cookies gesetzt, alle technisch notwendig für den Betrieb.
Es gibt kein Tracking-, Werbe- oder Analyse-Cookie und deshalb auch kein
Cookie-Banner.
cookies_title: Cookies
host: Der Server wird in unserem Auftrag von %host% betrieben, als Auftragsverarbeiter auf Grundlage eines Auftragsverarbeitungsvertrags.
logs: >-
Wie jeder Webserver protokolliert auch dieser jeden Abruf: die IP-Adresse,
von der er kam, den Zeitpunkt, die angeforderte Seite, die verweisende Seite
und die Browserkennung. Das ist für den Betrieb und die Absicherung des
Dienstes erforderlich (Art. 6 Abs. 1 lit. f DSGVO — berechtigtes Interesse).
Diese Protokolle werden rotiert und nicht zur Profilbildung verwendet.
logs_title: Server-Logfiles
rights: >-
Du hast das Recht auf Auskunft über die zu dir gespeicherten Daten sowie auf
deren Berichtigung, Löschung oder Einschränkung der Verarbeitung, außerdem
ein Widerspruchsrecht und ein Recht auf Datenübertragbarkeit. Wende dich
dafür an die oben genannte Adresse.
rights_title: Deine Rechte
third_parties: >-
Es werden keine Daten an Dritte weitergegeben. Diese Seite lädt keine
externen Schriftarten, Skripte, Karten oder Analysedienste — ihre Content
Security Policy erlaubt Verbindungen ausschließlich zu diesem Server.
third_parties_title: Weitergabe an Dritte
title: Datenschutzerklärung
unavailable:
title: In der Demo nicht verfügbar
body: >-
+41
View File
@@ -1307,6 +1307,47 @@ demo:
blurb: Take a look around plMail with a mailbox of your own — no sign-up, no address needed.
start: Start the demo
disclaimer: Your demo mailbox is deleted automatically after a couple of hours.
privacy:
account: >-
Starting the demo creates a throwaway account with a randomly generated
address. You are not asked for a name, an email address or a password, and
none is stored. Anything you type into it — messages, drafts, calendar
entries — exists only in that account and is deleted with it, automatically,
about %ttl% after it was created. Mail written in the demo is never sent:
there is no connection to any mail server.
account_title: The demo account
complaint: >-
You also have the right to lodge a complaint with a data protection
supervisory authority.
controller_intro: Responsible for the processing described here:
controller_title: Controller
cookie_mercure: keeps the live-update connection open while you are signed in.
cookie_session: keeps you signed in to your demo account. Deleted when you close the browser.
cookie_ui: remembers the theme and language you picked.
cookies: >-
Three cookies are set, all of them strictly necessary to operate the
service. There is no tracking, advertising or analytics cookie, and
therefore no consent banner.
cookies_title: Cookies
host: The server is operated on our behalf by %host%, acting as a processor under a data processing agreement.
logs: >-
Like any web server, this one records each request: the IP address it came
from, the time, the page requested, the referring page and the browser's
user-agent string. This is necessary to run the service and to defend it
against attack (Art. 6(1)(f) GDPR — legitimate interest). These logs are
rotated and are not used to build a profile of anyone.
logs_title: Server log files
rights: >-
You have the right to information about the data held about you, and to its
correction, erasure or restriction, as well as the right to object to
processing and the right to data portability. Write to the address above.
rights_title: Your rights
third_parties: >-
No data is passed to third parties. This site loads no external fonts,
scripts, maps or analytics — its content security policy allows connections
to this server only.
third_parties_title: Third parties
title: Privacy notice
unavailable:
title: Not available in the demo
body: >-
+39
View File
@@ -1032,6 +1032,45 @@ demo:
blurb: Take a turn about plMail wi' a sea chest o' yer own — no articles t' sign, no berth needed.
start: Begin th' sea trial
disclaimer: Yer trial chest be scuttled on its own after a watch or two.
privacy:
account: >-
Beginnin' th' sea trial opens a borrowed berth under a name drawn from th'
hat. Ye be asked for no name, no address an' no countersign, an' none be
kept. Whatever ye write there — letters, drafts, almanac marks — lives only
in that berth an' be scuttled wi' it, on its own, some %ttl% after it were
opened. Letters penned on a sea trial never leave th' harbour: there be no
line to any post at all.
account_title: Th' trial berth
complaint: Ye may also lay a complaint before th' harbourmaster o' records.
controller_intro: Answerable for th' handlin' set down here:
controller_title: Th' one answerable
cookie_mercure: keeps th' speakin'-line open while ye be aboard.
cookie_session: keeps ye aboard yer trial berth. Struck when ye close th' spyglass.
cookie_ui: remembers th' colours an' tongue ye chose.
cookies: >-
Three tokens be set, every one needful for sailin' her. There be no
followin' token, no crier's token an' no countin' token, an' so no banner
to wave away.
cookies_title: Tokens
host: Th' hull be kept on our behalf by %host%, under articles o' handlin'.
logs: >-
As any harbour does, this one notes each callin': th' address it came from,
th' hour, th' page sought, th' page afore it an' th' spyglass's mark. This
be needful to sail her an' to hold off boarders (Art. 6(1)(f) o' th'
articles — rightful interest). Th' logs be turned over an' no man's likeness
be built from them.
logs_title: Th' harbour log
rights: >-
Ye have th' right to be told what be held on ye, an' to have it set right,
struck, or held back, as well as th' right to object an' th' right to carry
yer records elsewhere. Write to th' address above.
rights_title: Yer rights
third_parties: >-
Naught be passed to other ships. This page draws no outside letterin',
scripts, charts nor countin'-houses — her articles permit lines to this
hull alone.
third_parties_title: Other ships
title: Articles o' records
unavailable:
title: Not to be had on a sea trial
body: >-