mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
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:
1 parent
d51c3c8475
commit
748e1cdcdf
19 files changed
+438
-4
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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') }}">
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
Reference in new issue
Block a user