Matomo-Tracking mit Einwilligung einbauen
This commit is contained in:
@@ -47,6 +47,7 @@ $isSwitch = !empty($current['stripe_subscription_id']);
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -97,6 +98,7 @@ $isSwitch = !empty($current['stripe_subscription_id']);
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -24,6 +25,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
const APP_ANALYTICS_CONSENT_COOKIE = 'kaffeeliste_analytics_consent';
|
||||||
|
const APP_ANALYTICS_CONSENT_VERSION = '1';
|
||||||
|
const APP_ANALYTICS_CONSENT_MAX_AGE = 15552000; // 180 Tage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
function app_matomo_configuration_errors(): array
|
||||||
|
{
|
||||||
|
$url = trim((string)app_env('MATOMO_URL', ''));
|
||||||
|
$siteId = trim((string)app_env('MATOMO_SITE_ID', ''));
|
||||||
|
$privacyConfirmed = trim((string)app_env('MATOMO_PRIVACY_CONFIRMED', ''));
|
||||||
|
$cookieDomain = trim((string)app_env('MATOMO_CONSENT_COOKIE_DOMAIN', ''));
|
||||||
|
$hasAnySetting = $url !== '' || $siteId !== '' || $cookieDomain !== '';
|
||||||
|
|
||||||
|
if (!$hasAnySetting) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$urlParts = $url !== '' ? parse_url($url) : false;
|
||||||
|
if (
|
||||||
|
$url === ''
|
||||||
|
|| filter_var($url, FILTER_VALIDATE_URL) === false
|
||||||
|
|| !is_array($urlParts)
|
||||||
|
|| !in_array(strtolower((string)($urlParts['scheme'] ?? '')), ['http', 'https'], true)
|
||||||
|
|| trim((string)($urlParts['host'] ?? '')) === ''
|
||||||
|
|| isset($urlParts['user'])
|
||||||
|
|| isset($urlParts['pass'])
|
||||||
|
|| isset($urlParts['query'])
|
||||||
|
|| isset($urlParts['fragment'])
|
||||||
|
) {
|
||||||
|
$errors[] = 'MATOMO_URL muss eine vollständige HTTP(S)-Basis-URL ohne Query oder Fragment sein.';
|
||||||
|
} elseif (!app_is_dev() && strtolower((string)$urlParts['scheme']) !== 'https') {
|
||||||
|
$errors[] = 'MATOMO_URL muss außerhalb der Entwicklungsumgebung HTTPS verwenden.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^[1-9][0-9]*$/', $siteId) !== 1) {
|
||||||
|
$errors[] = 'MATOMO_SITE_ID muss eine positive ganze Zahl sein.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($privacyConfirmed !== '1') {
|
||||||
|
$errors[] = 'MATOMO_PRIVACY_CONFIRMED muss nach Prüfung der Matomo-Datenschutzeinstellungen auf 1 gesetzt werden.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cookieDomainHostname = ltrim(strtolower($cookieDomain), '.');
|
||||||
|
if ($cookieDomain !== '' && filter_var($cookieDomainHostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
|
||||||
|
$errors[] = 'MATOMO_CONSENT_COOKIE_DOMAIN ist keine gültige Cookie-Domain.';
|
||||||
|
} elseif ($cookieDomain !== '') {
|
||||||
|
$hostBelongsToCookieDomain = static function (string $host) use ($cookieDomainHostname): bool {
|
||||||
|
$host = strtolower($host);
|
||||||
|
return $host === $cookieDomainHostname || str_ends_with($host, '.' . $cookieDomainHostname);
|
||||||
|
};
|
||||||
|
$appHost = trim((string)app_primary_host());
|
||||||
|
$marketingHost = (string)parse_url((string)app_env('APP_MARKETING_URL', ''), PHP_URL_HOST);
|
||||||
|
if ($appHost !== '' && !$hostBelongsToCookieDomain($appHost)) {
|
||||||
|
$errors[] = 'MATOMO_CONSENT_COOKIE_DOMAIN passt nicht zu APP_HOST.';
|
||||||
|
}
|
||||||
|
if ($marketingHost !== '' && !$hostBelongsToCookieDomain($marketingHost)) {
|
||||||
|
$errors[] = 'MATOMO_CONSENT_COOKIE_DOMAIN passt nicht zu APP_MARKETING_URL.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{matomoUrl: string, siteId: string, consentCookie: string, consentVersion: string, consentMaxAge: int, cookieDomain: string, secureCookies: bool}|null
|
||||||
|
*/
|
||||||
|
function app_matomo_configuration(): ?array
|
||||||
|
{
|
||||||
|
$url = trim((string)app_env('MATOMO_URL', ''));
|
||||||
|
$siteId = trim((string)app_env('MATOMO_SITE_ID', ''));
|
||||||
|
$privacyConfirmed = trim((string)app_env('MATOMO_PRIVACY_CONFIRMED', ''));
|
||||||
|
|
||||||
|
if ($url === '' && $siteId === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (app_matomo_configuration_errors() !== []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'matomoUrl' => rtrim($url, '/') . '/',
|
||||||
|
'siteId' => $siteId,
|
||||||
|
'consentCookie' => APP_ANALYTICS_CONSENT_COOKIE,
|
||||||
|
'consentVersion' => APP_ANALYTICS_CONSENT_VERSION,
|
||||||
|
'consentMaxAge' => APP_ANALYTICS_CONSENT_MAX_AGE,
|
||||||
|
'cookieDomain' => trim((string)app_env('MATOMO_CONSENT_COOKIE_DOMAIN', '')),
|
||||||
|
'secureCookies' => app_is_https(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_analytics_head_html(): string
|
||||||
|
{
|
||||||
|
if (app_matomo_configuration() === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<link rel="stylesheet" href="assets/css/analytics-consent.css">' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_analytics_body_html(): string
|
||||||
|
{
|
||||||
|
$configuration = app_matomo_configuration();
|
||||||
|
if ($configuration === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode(
|
||||||
|
$configuration,
|
||||||
|
JSON_UNESCAPED_SLASHES
|
||||||
|
| JSON_UNESCAPED_UNICODE
|
||||||
|
| JSON_HEX_TAG
|
||||||
|
| JSON_HEX_AMP
|
||||||
|
| JSON_HEX_APOS
|
||||||
|
| JSON_HEX_QUOT
|
||||||
|
);
|
||||||
|
if (!is_string($json)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <<<'HTML'
|
||||||
|
<section class="analytics-consent" id="analytics-consent" aria-labelledby="analytics-consent-title" aria-describedby="analytics-consent-description" hidden>
|
||||||
|
<div class="analytics-consent__inner">
|
||||||
|
<div class="analytics-consent__copy">
|
||||||
|
<h2 id="analytics-consent-title">Optionale Nutzungsanalyse</h2>
|
||||||
|
<p id="analytics-consent-description">Wir möchten mit Matomo verstehen, wie unsere Website und App genutzt werden. Erst nach deiner Zustimmung laden wir Matomo und setzen Analyse-Cookies. Notwendige Cookies funktionieren immer. Mehr dazu steht im <a href="datenschutz.php">Datenschutz</a>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="analytics-consent__actions">
|
||||||
|
<button type="button" data-analytics-consent="denied">Nur notwendige</button>
|
||||||
|
<button type="button" data-analytics-consent="granted">Analyse erlauben</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<button type="button" class="analytics-consent-settings" id="analytics-consent-settings" hidden>Cookie-Einstellungen</button>
|
||||||
|
HTML
|
||||||
|
. "\n<script type=\"application/json\" id=\"analytics-configuration\">{$json}</script>\n"
|
||||||
|
. '<script src="assets/js/analytics-consent.js" defer></script>' . "\n";
|
||||||
|
}
|
||||||
@@ -203,3 +203,8 @@ function app_require_csrf(): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
app_send_security_headers();
|
app_send_security_headers();
|
||||||
|
|
||||||
|
// Der Analyse-Baustein wird zentral geladen, damit alle HTML-Templates nur
|
||||||
|
// noch die beiden bewusst platzierten Head-/Body-Fragmente ausgeben muessen.
|
||||||
|
// analytics.php bindet bootstrap.php seinerseits per require_once ein.
|
||||||
|
require_once __DIR__ . '/analytics.php';
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
require_once __DIR__ . '/bootstrap.php';
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
const APP_TERMS_VERSION = '2026-08-22';
|
const APP_TERMS_VERSION = '2026-08-22';
|
||||||
const APP_PRIVACY_VERSION = '2026-08-22';
|
const APP_PRIVACY_VERSION = '2026-08-27';
|
||||||
const APP_DPA_VERSION = '2026-08-22';
|
const APP_DPA_VERSION = '2026-08-22';
|
||||||
const APP_WITHDRAWAL_VERSION = '2026-08-22';
|
const APP_WITHDRAWAL_VERSION = '2026-08-22';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
.analytics-consent[hidden],
|
||||||
|
.analytics-consent-settings[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: solid 1px rgba(210, 215, 217, 0.95);
|
||||||
|
box-shadow: 0 -0.75rem 2rem rgba(61, 68, 73, 0.14);
|
||||||
|
color: #3d4449;
|
||||||
|
padding: 1.25rem 6vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__copy {
|
||||||
|
max-width: 48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent h2 {
|
||||||
|
color: #3d4449;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent a {
|
||||||
|
color: #2f6418;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions button,
|
||||||
|
.analytics-consent-settings {
|
||||||
|
appearance: none;
|
||||||
|
background: #38761d;
|
||||||
|
border: solid 2px #38761d;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: none;
|
||||||
|
color: #ffffff !important;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
height: auto;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.25;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
text-transform: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions button:hover,
|
||||||
|
.analytics-consent-settings:hover {
|
||||||
|
background: #2f6418;
|
||||||
|
border-color: #2f6418;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions button:focus-visible,
|
||||||
|
.analytics-consent-settings:focus-visible {
|
||||||
|
outline: solid 3px #f0b429;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent-settings {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
z-index: 9999;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #2f6418 !important;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 736px) {
|
||||||
|
.analytics-consent {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__inner,
|
||||||
|
.analytics-consent__actions {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__inner {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-consent__actions button {
|
||||||
|
width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var configurationElement = document.getElementById('analytics-configuration');
|
||||||
|
var banner = document.getElementById('analytics-consent');
|
||||||
|
var settingsButton = document.getElementById('analytics-consent-settings');
|
||||||
|
|
||||||
|
if (!configurationElement || !banner || !settingsButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuration;
|
||||||
|
try {
|
||||||
|
configuration = JSON.parse(configurationElement.textContent || '');
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookieName = configuration.consentCookie;
|
||||||
|
var consentVersion = configuration.consentVersion;
|
||||||
|
var matomoStarted = false;
|
||||||
|
|
||||||
|
function readDecision() {
|
||||||
|
var cookies = document.cookie ? document.cookie.split(';') : [];
|
||||||
|
for (var index = 0; index < cookies.length; index += 1) {
|
||||||
|
var parts = cookies[index].trim().split('=');
|
||||||
|
var name = decodeURIComponent(parts.shift() || '');
|
||||||
|
if (name !== cookieName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = decodeURIComponent(parts.join('='));
|
||||||
|
var expectedPrefix = consentVersion + ':';
|
||||||
|
if (value.indexOf(expectedPrefix) !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decision = value.slice(expectedPrefix.length);
|
||||||
|
return decision === 'granted' || decision === 'denied' ? decision : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cookieSuffix() {
|
||||||
|
var suffix = '; Path=/; Max-Age=' + String(configuration.consentMaxAge) + '; SameSite=Lax';
|
||||||
|
if (configuration.cookieDomain) {
|
||||||
|
suffix += '; Domain=' + configuration.cookieDomain;
|
||||||
|
}
|
||||||
|
if (configuration.secureCookies || window.location.protocol === 'https:') {
|
||||||
|
suffix += '; Secure';
|
||||||
|
}
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDecision(decision) {
|
||||||
|
document.cookie = encodeURIComponent(cookieName)
|
||||||
|
+ '=' + encodeURIComponent(consentVersion + ':' + decision)
|
||||||
|
+ cookieSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
function URLWithoutParameters(value) {
|
||||||
|
try {
|
||||||
|
var url = new URL(value, window.location.href);
|
||||||
|
return url.origin + url.pathname;
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMatomo() {
|
||||||
|
window._paq = window._paq || [];
|
||||||
|
|
||||||
|
if (matomoStarted) {
|
||||||
|
window._paq.push(['setConsentGiven']);
|
||||||
|
window._paq.push(['trackPageView']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
matomoStarted = true;
|
||||||
|
|
||||||
|
var trackerUrl = configuration.matomoUrl + 'matomo.php';
|
||||||
|
window._paq.push(['requireConsent']);
|
||||||
|
window._paq.push(['setTrackerUrl', trackerUrl]);
|
||||||
|
window._paq.push(['setSiteId', configuration.siteId]);
|
||||||
|
window._paq.push(['setDoNotTrack', true]);
|
||||||
|
window._paq.push(['disableBrowserFeatureDetection']);
|
||||||
|
window._paq.push(['disablePerformanceTracking']);
|
||||||
|
window._paq.push(['setCookiePath', '/']);
|
||||||
|
window._paq.push(['setCookieSameSite', 'Lax']);
|
||||||
|
window._paq.push(['setSecureCookie', window.location.protocol === 'https:']);
|
||||||
|
window._paq.push(['setVisitorCookieTimeout', configuration.consentMaxAge]);
|
||||||
|
window._paq.push(['setReferralCookieTimeout', configuration.consentMaxAge]);
|
||||||
|
window._paq.push(['setCustomUrl', URLWithoutParameters(window.location.href)]);
|
||||||
|
|
||||||
|
var referrer = URLWithoutParameters(document.referrer);
|
||||||
|
if (referrer) {
|
||||||
|
window._paq.push(['setReferrerUrl', referrer]);
|
||||||
|
}
|
||||||
|
if (configuration.cookieDomain) {
|
||||||
|
window._paq.push(['setCookieDomain', configuration.cookieDomain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
window._paq.push(['setConsentGiven']);
|
||||||
|
window._paq.push(['trackPageView']);
|
||||||
|
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.src = configuration.matomoUrl + 'matomo.js';
|
||||||
|
script.referrerPolicy = 'strict-origin-when-cross-origin';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expireCookie(name, domain) {
|
||||||
|
var value = encodeURIComponent(name) + '=; Path=/; Max-Age=0; SameSite=Lax';
|
||||||
|
if (domain) {
|
||||||
|
value += '; Domain=' + domain;
|
||||||
|
}
|
||||||
|
if (configuration.secureCookies || window.location.protocol === 'https:') {
|
||||||
|
value += '; Secure';
|
||||||
|
}
|
||||||
|
document.cookie = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMatomoCookies() {
|
||||||
|
var cookies = document.cookie ? document.cookie.split(';') : [];
|
||||||
|
cookies.forEach(function (cookie) {
|
||||||
|
var name = decodeURIComponent(cookie.trim().split('=')[0] || '');
|
||||||
|
if (name.indexOf('_pk_') !== 0 && name.indexOf('mtm_') !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expireCookie(name, '');
|
||||||
|
if (configuration.cookieDomain) {
|
||||||
|
expireCookie(name, configuration.cookieDomain);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopMatomo() {
|
||||||
|
if (window._paq) {
|
||||||
|
window._paq.push(['forgetConsentGiven']);
|
||||||
|
window._paq.push(['deleteCookies']);
|
||||||
|
}
|
||||||
|
removeMatomoCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBanner() {
|
||||||
|
banner.hidden = true;
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(decision) {
|
||||||
|
writeDecision(decision);
|
||||||
|
closeBanner();
|
||||||
|
if (decision === 'granted') {
|
||||||
|
startMatomo();
|
||||||
|
} else {
|
||||||
|
stopMatomo();
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('kaffeeliste:analytics-consent', {
|
||||||
|
detail: {analytics: decision}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
banner.querySelectorAll('[data-analytics-consent]').forEach(function (button) {
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
choose(button.getAttribute('data-analytics-consent'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsButton.addEventListener('click', function () {
|
||||||
|
banner.hidden = false;
|
||||||
|
settingsButton.hidden = true;
|
||||||
|
var firstButton = banner.querySelector('[data-analytics-consent="denied"]');
|
||||||
|
if (firstButton) {
|
||||||
|
firstButton.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var initialDecision = readDecision();
|
||||||
|
if (initialDecision === 'granted') {
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
startMatomo();
|
||||||
|
} else if (initialDecision === 'denied') {
|
||||||
|
settingsButton.hidden = false;
|
||||||
|
stopMatomo();
|
||||||
|
} else {
|
||||||
|
banner.hidden = false;
|
||||||
|
}
|
||||||
|
}());
|
||||||
@@ -9,6 +9,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -24,6 +25,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -24,6 +25,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -345,6 +345,52 @@ reichen, ist das die nächste Stufe.
|
|||||||
|
|
||||||
Abgedeckt von `scripts/check-spam-guard.php` (ohne Datenbank lauffähig).
|
Abgedeckt von `scripts/check-spam-guard.php` (ohne Datenbank lauffähig).
|
||||||
|
|
||||||
|
## Matomo und Analyse-Einwilligung
|
||||||
|
|
||||||
|
Die Anwendung enthält eine eigene Einwilligungsoberfläche und lädt Matomo
|
||||||
|
erst nach „Analyse erlauben“. Ohne vollständige Konfiguration wird weder die
|
||||||
|
Oberfläche ausgegeben noch eine Verbindung zu Matomo aufgebaut. Für die
|
||||||
|
Produktion in `env.local.php` ergänzen:
|
||||||
|
|
||||||
|
```php
|
||||||
|
putenv('MATOMO_URL=https://analytics.kaffeeliste.de/');
|
||||||
|
putenv('MATOMO_SITE_ID=1');
|
||||||
|
putenv('MATOMO_CONSENT_COOKIE_DOMAIN=.kaffeeliste.de');
|
||||||
|
putenv('MATOMO_PRIVACY_CONFIRMED=1');
|
||||||
|
```
|
||||||
|
|
||||||
|
`MATOMO_SITE_ID` durch die numerische Website-ID ersetzen. Die gemeinsame
|
||||||
|
Cookie-Domain ist nur richtig, wenn Marketingseite, App und Matomo tatsächlich
|
||||||
|
unter `kaffeeliste.de` betrieben werden sollen. Sonst leer lassen; die
|
||||||
|
Einwilligung gilt dann nur auf dem jeweils besuchten Host.
|
||||||
|
|
||||||
|
`MATOMO_PRIVACY_CONFIRMED=1` erst setzen, nachdem die Matomo-Instanz so
|
||||||
|
konfiguriert und geprüft wurde:
|
||||||
|
|
||||||
|
1. Matomo wird selbst auf der im Datenschutzhinweis genannten EU-Infrastruktur
|
||||||
|
betrieben; Matomo Cloud oder ein anderer Empfänger ist nicht eingetragen.
|
||||||
|
2. Unter Datenschutz/Anonymisierung werden IP-Adressen vor Speicherung um
|
||||||
|
mindestens zwei Bytes gekürzt.
|
||||||
|
3. Roh- und Besuchsprotokolle werden spätestens nach 180 Tagen automatisch
|
||||||
|
gelöscht; verdichtete Berichte spätestens nach 25 Monaten.
|
||||||
|
4. Keine User-ID, benutzerbezogene Custom Dimensions, Heatmaps,
|
||||||
|
Sitzungsaufzeichnungen oder Werbeintegrationen aktivieren. Der eingebundene
|
||||||
|
Tracker deaktiviert zusätzlich Browser-Funktionserkennung,
|
||||||
|
Performance-Messung und Link-Klick-Tracking.
|
||||||
|
5. In Matomo ist genau eine Website mit den erlaubten Marketing- und
|
||||||
|
App-Domains angelegt. Testaufrufe mit Query-Parametern dürfen in Matomo nur
|
||||||
|
den Pfad ohne Query oder Fragment zeigen.
|
||||||
|
6. Im privaten Browserfenster prüfen: Vor der Entscheidung und nach „Nur
|
||||||
|
notwendige“ gibt es keine Anfrage an `matomo.js`/`matomo.php` und keine
|
||||||
|
`_pk_*`-Cookies. Nach Zustimmung gibt es beides; ein anschließender Widerruf
|
||||||
|
über „Cookie-Einstellungen“ stoppt weitere Aufrufe und löscht die Cookies.
|
||||||
|
|
||||||
|
Die Entscheidung wird 180 Tage im Cookie
|
||||||
|
`kaffeeliste_analytics_consent` gespeichert. Eine Versionsänderung in
|
||||||
|
`APP_ANALYTICS_CONSENT_VERSION` fragt erneut. Änderungen an Zweck, Datenumfang
|
||||||
|
oder Empfängern erfordern außerdem eine neue Datenschutzerklärung und eine
|
||||||
|
erneute rechtliche Prüfung.
|
||||||
|
|
||||||
## Betreiber-Benachrichtigung bei neuer Registrierung
|
## Betreiber-Benachrichtigung bei neuer Registrierung
|
||||||
|
|
||||||
`register.php` schickt nach einer erfolgreichen Selbstregistrierung einen
|
`register.php` schickt nach einer erfolgreichen Selbstregistrierung einen
|
||||||
@@ -465,6 +511,9 @@ Rechten der Secret-Dateien oder einer nicht angewendeten Legal-Migration.
|
|||||||
Test von Neubuchung, Tarifwechsel und Kündigung zum Periodenende
|
Test von Neubuchung, Tarifwechsel und Kündigung zum Periodenende
|
||||||
- [ ] Webserver-Access-Logs werden spätestens nach sieben Tagen gelöscht oder
|
- [ ] Webserver-Access-Logs werden spätestens nach sieben Tagen gelöscht oder
|
||||||
anonymisiert; TLS/HSTS ist auf allen öffentlichen Domains aktiv
|
anonymisiert; TLS/HSTS ist auf allen öffentlichen Domains aktiv
|
||||||
|
- [ ] Falls Matomo aktiv ist: Datenschutz-Checkliste oben vollständig geprüft,
|
||||||
|
Opt-in/Ablehnung/Widerruf im Browser getestet und
|
||||||
|
`MATOMO_PRIVACY_CONFIRMED=1` gesetzt
|
||||||
- [ ] Täglicher Task `php scripts/purge-ended-contracts.php` ist eingerichtet;
|
- [ ] Täglicher Task `php scripts/purge-ended-contracts.php` ist eingerichtet;
|
||||||
ein Vorlauf mit `--dry-run` wurde kontrolliert
|
ein Vorlauf mit `--dry-run` wurde kontrolliert
|
||||||
- [ ] Stündlicher Task `php scripts/purge-expired-operational-data.php` ist
|
- [ ] Stündlicher Task `php scripts/purge-expired-operational-data.php` ist
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ $result = $token !== ''
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -69,6 +70,7 @@ $result = $token !== ''
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ putenv('APP_BASE_URL=https://app.kaffeeliste.de');
|
|||||||
// App-Subdomain wegfaellt - hier also https://kaffeeliste.de.
|
// App-Subdomain wegfaellt - hier also https://kaffeeliste.de.
|
||||||
putenv('APP_MARKETING_URL=https://kaffeeliste.de');
|
putenv('APP_MARKETING_URL=https://kaffeeliste.de');
|
||||||
|
|
||||||
|
// Optionale, erst nach Einwilligung geladene Nutzungsanalyse. Beide Domains
|
||||||
|
// koennen dieselbe Entscheidung und Matomo-Besucherkennung verwenden, wenn
|
||||||
|
// die Cookie-Domain auf die gemeinsame Eltern-Domain gesetzt wird.
|
||||||
|
// Vor MATOMO_PRIVACY_CONFIRMED=1 die Checkliste in docs/deployment.md
|
||||||
|
// (IP-Kuerzung, Loeschfristen, keine Heatmaps/Sitzungsaufzeichnung) abarbeiten.
|
||||||
|
// Fehlen URL und Site-ID, ist Matomo vollstaendig deaktiviert und es erscheint
|
||||||
|
// kein Einwilligungsbanner.
|
||||||
|
putenv('MATOMO_URL='); // Beispiel: https://analytics.kaffeeliste.de/
|
||||||
|
putenv('MATOMO_SITE_ID='); // Numerische Website-ID aus Matomo
|
||||||
|
putenv('MATOMO_CONSENT_COOKIE_DOMAIN='); // Produktion: .kaffeeliste.de
|
||||||
|
putenv('MATOMO_PRIVACY_CONFIRMED=0');
|
||||||
|
|
||||||
putenv('APP_DB_DRIVER=mysql');
|
putenv('APP_DB_DRIVER=mysql');
|
||||||
putenv('DB_HOST=');
|
putenv('DB_HOST=');
|
||||||
putenv('DB_PORT=3306');
|
putenv('DB_PORT=3306');
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ if ($saasNavTenantId > 0) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/app.css" />
|
<link rel="stylesheet" href="assets/css/app.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/app/ledger.php';
|
require_once __DIR__ . '/app/ledger.php';
|
||||||
@@ -74,4 +75,4 @@ if ($headerSaasUser !== null && ($headerSaasUser['email_verified_at'] ?? null) =
|
|||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
|
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -145,6 +146,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ $jsonFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
|
|||||||
<link rel="preload" as="image" href="assets/images/landing-hero.png" />
|
<link rel="preload" as="image" href="assets/images/landing-hero.png" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
<script type="application/ld+json"><?php echo json_encode($productLd, $jsonFlags); ?></script>
|
<script type="application/ld+json"><?php echo json_encode($productLd, $jsonFlags); ?></script>
|
||||||
<script type="application/ld+json"><?php echo json_encode($faqLd, $jsonFlags); ?></script>
|
<script type="application/ld+json"><?php echo json_encode($faqLd, $jsonFlags); ?></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -290,6 +291,7 @@ $jsonFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
## 1. Verantwortlicher und Kontakt
|
||||||
|
|
||||||
|
Verantwortlich für die eigenen Verarbeitungen des Kaffeeliste-Dienstes ist Clemens Creutzburg, CTB-IT, In den Sieben Stücken 9d, 30655 Hannover, E-Mail: info@ctb-it.de. Telefonnummer und Ticketsystem stehen im [Impressum](impressum.php). Ein Datenschutzbeauftragter ist derzeit nicht bestellt, weil keine gesetzliche Bestellpflicht festgestellt wurde.
|
||||||
|
|
||||||
|
## 2. Unsere Rollen
|
||||||
|
|
||||||
|
Für Registrierung, Kundenkonto, Vertragsabwicklung, Abrechnung, Support, Sicherheit, die optionale Nutzungsanalyse und den Betrieb der öffentlichen Website ist CTB-IT Verantwortlicher.
|
||||||
|
|
||||||
|
Verwaltet ein Kunde Daten anderer Personen innerhalb seines Mandanten, ist grundsätzlich der Kunde Verantwortlicher und CTB-IT Auftragsverarbeiter. Dies regelt der [AVV](avv.php). Bei ausschließlich persönlichen oder familiären Tätigkeiten eines privaten Kunden kann die Haushaltsausnahme nach Art. 2 Abs. 2 lit. c DSGVO gelten. CTB-IT schützt die Daten technisch und vertraglich unabhängig davon.
|
||||||
|
|
||||||
|
## 3. Website, Serverprotokolle und Sicherheit
|
||||||
|
|
||||||
|
Beim Aufruf werden technisch erforderliche Verbindungsdaten verarbeitet: IP-Adresse, Datum und Uhrzeit, angeforderte Adresse, übertragene Datenmenge, Referrer, Browser, Betriebssystem und HTTP-Status. Zweck sind sichere Auslieferung, Fehleranalyse und Abwehr von Angriffen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; das berechtigte Interesse ist ein sicherer und stabiler Dienst.
|
||||||
|
|
||||||
|
Anwendungsseitige IP-Daten aus der Begrenzung automatisierter Versuche werden spätestens nach 24 Stunden gelöscht. Webserver-Protokolle sollen im Produktivbetrieb nach spätestens sieben Tagen gelöscht oder anonymisiert werden, sofern ein Sicherheitsvorfall keine längere, zweckgebundene Sicherung erfordert.
|
||||||
|
|
||||||
|
## 4. Registrierung und Kundenkonto
|
||||||
|
|
||||||
|
Wir verarbeiten Kundenname und -kürzel, Kundentyp, Name, E-Mail-Adresse, Passwort-Hash, E-Mail-Bestätigung, Rollen, Anmeldezeitpunkte, Vertrags- und Rechtstextversionen, Zeitpunkt der Annahme, IP-Adresse und User-Agent. Zwecke sind Vertragsschluss, Authentifizierung, Berechtigungen, Vertragsnachweis und Kommunikation. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b DSGVO sowie für Sicherheits- und Nachweisdaten Art. 6 Abs. 1 lit. f DSGVO. Pflichtfelder sind für Vertragsschluss und sichere Kontoführung erforderlich; ohne sie ist keine Registrierung möglich.
|
||||||
|
|
||||||
|
Ein Klartextpasswort wird nicht gespeichert. Die Kenntnisnahme der Datenschutzerklärung ist keine Einwilligung in Werbung oder Analyse. Eine Einwilligung in die Nutzungsanalyse wird separat über die Cookie-Einstellungen abgefragt.
|
||||||
|
|
||||||
|
## 5. Mandanten-, Teilnehmer- und Kassendaten
|
||||||
|
|
||||||
|
Je nach Nutzung werden Einstellungen, Marken- und Zahlungsangaben des Kunden, Teilnehmername, E-Mail-Adresse, PayPal-Anzeigename und -E-Mail, Nutzerzuordnung, Rollen, Aktivstatus, Verbrauch, Einzahlungen, Salden, Bemerkungen, Hinweise, FAQ-Inhalte, Importe, Benachrichtigungsstatus und Administrationsprotokolle verarbeitet.
|
||||||
|
|
||||||
|
Bei Auftragsverarbeitung bestimmt der Kunde Zwecke, Rechtsgrundlagen, Inhalte und Empfänger. Die Daten stammen vom Kunden, seinen Administratoren, den Teilnehmern oder aus vom Kunden veranlassten Importen. Teilnehmer sollten sich für Auskunft, Berichtigung oder Löschung zuerst an den jeweiligen Kunden wenden; CTB-IT unterstützt diesen nach dem AVV.
|
||||||
|
|
||||||
|
## 6. Vertrag, Abrechnung, Stripe und Dolibarr
|
||||||
|
|
||||||
|
Bei kostenpflichtigen Tarifen verarbeiten wir Tarif, Teilnehmerzahl, Abrechnungsstatus, Stripe-Kunden- und Abonnementkennung, Laufzeit, Rechnungsbetrag und Zahlungsstatus zur Vertragsdurchführung nach Art. 6 Abs. 1 lit. b DSGVO und für Buchführung und Steuerrecht nach Art. 6 Abs. 1 lit. c DSGVO.
|
||||||
|
|
||||||
|
Zahlungen werden durch Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, Irland, abgewickelt. Stripe erhält insbesondere Kontakt-, Vertrags-, Geräte- und Zahlungsdaten. Stripe verarbeitet einen Teil als unser Auftragsverarbeiter und einen Teil in eigener Verantwortlichkeit, etwa zur Betrugsprävention und Erfüllung finanzrechtlicher Pflichten. Übermittlungen in die USA oder andere Drittländer können auf Angemessenheitsbeschlüssen, insbesondere dem EU-US Data Privacy Framework für zertifizierte Empfänger, oder EU-Standardvertragsklauseln beruhen. Weitere Informationen: [Stripe-Datenschutz](https://stripe.com/de/privacy).
|
||||||
|
|
||||||
|
Bezahlte Rechnungen werden im selbst betriebenen Dolibarr-System unter verwaltung.ctb-it.de für Buchführung und Kundenverwaltung gespiegelt. Dabei werden Kunden-, Vertrags-, Rechnungs- und Zahlungsdaten verarbeitet, aber grundsätzlich keine Teilnehmer- oder Kasseninhalte.
|
||||||
|
|
||||||
|
## 7. PayPal-Zahlungsmails
|
||||||
|
|
||||||
|
Aktiviert ein Kunde die PayPal-Zuordnung, werden an die mandantenspezifische Zahlungsadresse weitergeleitete PayPal-Benachrichtigungen automatisiert ausgelesen. Verarbeitet werden unter anderem Absender- und Empfängeradresse, PayPal-Transaktionskennung, Name und E-Mail des Zahlers, Betrag, Währung, Zahlungszeit und Zuordnungsstatus. Zweck ist die vom Kunden gewünschte Zuordnung zu Teilnehmern und Einzahlungen. Bei Teilnehmerdaten erfolgt dies im Auftrag des Kunden; für die Bereitstellung und Sicherheit der Funktion zusätzlich auf Art. 6 Abs. 1 lit. b und f DSGVO.
|
||||||
|
|
||||||
|
PayPal selbst verarbeitet Daten in eigener Verantwortlichkeit nach den dortigen Datenschutzinformationen. Die Kaffeeliste ruft keine PayPal-Kontodaten des Kunden über eine PayPal-API ab.
|
||||||
|
|
||||||
|
## 8. E-Mail, Support und Ticketsystem
|
||||||
|
|
||||||
|
Wir senden notwendige E-Mails zur Verifizierung, Anmeldung, Passwortzurücksetzung, Einladung, Vertragsbestätigung, Kündigung, Widerruf und zu vom Kunden aktivierten Kassenbenachrichtigungen. Verarbeitet werden Empfänger, Betreff, Status, Zeitpunkt und gegebenenfalls Fehler. Grundlage sind Art. 6 Abs. 1 lit. b DSGVO beziehungsweise bei Teilnehmernachrichten der Auftrag des Kunden. Versandprotokolle werden regelmäßig nach 180 Tagen gelöscht, soweit sie nicht als Vertragsnachweis länger erforderlich sind.
|
||||||
|
|
||||||
|
Bei Kontakt per E-Mail oder über ticketsystem.ctb-it.de verarbeiten wir die übermittelten Kontaktdaten, Inhalte, Anhänge, Zeitpunkte und technischen Protokolldaten, um die Anfrage zu bearbeiten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bei vorvertraglichen oder vertraglichen Anliegen, sonst Art. 6 Abs. 1 lit. f DSGVO. Unser Interesse ist nachvollziehbarer und sicherer Support. Bitte übermitteln Sie im Ticket keine unnötigen besonderen Kategorien personenbezogener Daten.
|
||||||
|
|
||||||
|
## 9. Hosting, Empfänger und Auftragsverarbeiter
|
||||||
|
|
||||||
|
Die Anwendung, Datenbank, Sitzungen, Sicherungen, der Mailtransport und die selbst betriebene Matomo-Instanz werden auf Infrastruktur der netcup GmbH, Emmy-Noether-Straße 10, 76131 Karlsruhe, Deutschland, innerhalb Deutschlands beziehungsweise der Europäischen Union betrieben. Mit netcup besteht ein Vertrag über Auftragsverarbeitung. Durch die Nutzungsanalyse werden keine Daten an Matomo Cloud übermittelt.
|
||||||
|
|
||||||
|
Weitere Empfänger sind Stripe für Zahlung und Abrechnung, Behörden oder Berater bei gesetzlicher Pflicht beziehungsweise Rechtsverteidigung und die vom Kunden bestimmten Nutzer und Empfänger. Eine Weitergabe für fremde Werbung findet nicht statt.
|
||||||
|
|
||||||
|
## 10. Cookies, Einwilligung und Matomo
|
||||||
|
|
||||||
|
Die Anwendung setzt das Sitzungs-Cookie „kaffeeliste_session“. Es ist für Login, Mandantenzuordnung, Sicherheit und CSRF-Schutz erforderlich, enthält eine zufällige Sitzungskennung, ist für JavaScript nicht lesbar und wird beim Schließen des Browsers beendet. Grundlage für den Zugriff auf das Endgerät ist § 25 Abs. 2 Nr. 2 TDDDG; die anschließende Verarbeitung beruht auf Art. 6 Abs. 1 lit. b und f DSGVO.
|
||||||
|
|
||||||
|
Das Cookie „kaffeeliste_analytics_consent“ speichert für 180 Tage ausschließlich die Version der Abfrage und die Entscheidung „erlaubt“ oder „abgelehnt“. Es enthält keine Benutzerkennung. Es ist erforderlich, um die gewünschte Einstellung umzusetzen und nicht bei jedem Seitenaufruf erneut zu fragen. Grundlage sind § 25 Abs. 2 Nr. 2 TDDDG und, soweit die Einstellung personenbezogen ist, Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist eine nutzerfreundliche und zuverlässige Einwilligungsverwaltung.
|
||||||
|
|
||||||
|
Nur nach einer freiwilligen Einwilligung verwenden wir die selbst betriebene Analysesoftware Matomo, um Aufrufe und Navigationswege auf der öffentlichen Website und in der App auszuwerten. Dabei können gekürzte IP-Adresse, Zeitpunkt, aufgerufener Seitenpfad und Seitentitel, zuvor besuchter Seitenpfad, Browser- und Geräteinformationen sowie eine zufällige Besucherkennung verarbeitet werden. URL-Abfragen und Fragmente werden vor der Übermittlung entfernt; dadurch werden insbesondere Verifizierungs-, Passwort-Reset- und Zugriffs-Tokens nicht an Matomo gesendet. Formulareingaben, Kasseninhalte und eine Kaffeeliste-Benutzerkennung werden nicht an Matomo übermittelt. Browser-Einstellungen „Do Not Track“ werden beachtet. Browser-Funktionserkennung, Performance-Messung, Link-Klick-Tracking, Heatmaps und Sitzungsaufzeichnungen sind nicht aktiviert.
|
||||||
|
|
||||||
|
Nach der Einwilligung kann Matomo insbesondere die Cookies „_pk_id.*“ zur Wiedererkennung eines Browsers für bis zu 180 Tage, „_pk_ses.*“ für eine Sitzung von regelmäßig 30 Minuten und gegebenenfalls „_pk_ref.*“ für Referrer-Informationen für bis zu 180 Tage setzen. Rechtsgrundlagen sind § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO. Die Einwilligung ist freiwillig; ihre Ablehnung hat keinen Einfluss auf die Nutzung des Dienstes.
|
||||||
|
|
||||||
|
Die IP-Adresse wird in Matomo vor ihrer dauerhaften Speicherung um mindestens zwei Bytes gekürzt. Roh- und Besuchsdaten werden spätestens nach 180 Tagen gelöscht; ausschließlich verdichtete Berichte werden höchstens 25 Monate aufbewahrt. Es findet kein websiteübergreifendes Tracking, keine Werbung und keine Weitergabe der Analysedaten an Dritte zu deren eigenen Zwecken statt.
|
||||||
|
|
||||||
|
Die Entscheidung kann jederzeit über die ständig erreichbare Schaltfläche „Cookie-Einstellungen“ geändert werden. Bei einem Widerruf wird die künftige Analyse beendet und erreichbare Matomo-Cookies werden gelöscht. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Verarbeitung bleibt unberührt.
|
||||||
|
|
||||||
|
Schriften und Gestaltungselemente werden lokal ausgeliefert; beim bloßen Seitenaufruf wird keine Verbindung zu Google Fonts hergestellt.
|
||||||
|
|
||||||
|
## 11. Speicherdauer und Löschung
|
||||||
|
|
||||||
|
- Kundenkonto und operative Mandantendaten: für die Vertragsdauer; bei einer ausdrücklich ausgelösten Mandantenlöschung sofort aus dem Produktivsystem, sonst 30 Tage nach Vertragsende
|
||||||
|
- technische Rate-Limit-Daten: höchstens 24 Stunden
|
||||||
|
- Admin- und Sicherheitsprotokoll: regelmäßig 180 Tage; bei einem konkreten Vorfall bis zu dessen Abschluss und Ablauf einschlägiger Verjährungsfristen
|
||||||
|
- E-Mail-Versandprotokoll: regelmäßig 180 Tage
|
||||||
|
- Matomo-Roh- und Besuchsdaten: höchstens 180 Tage; verdichtete Analyseberichte höchstens 25 Monate
|
||||||
|
- Vertragsannahmen, Kündigungen, Widerrufe und erforderliche Korrespondenz: regelmäßig bis zum Ablauf der zivilrechtlichen Verjährung, grundsätzlich drei Jahre ab Ende des Jahres, bei Rechtsstreit länger
|
||||||
|
- Rechnungen, Zahlungs- und Buchungsbelege des Anbieters: acht Jahre oder eine andere gesetzlich vorgeschriebene Dauer nach § 257 HGB und § 147 AO
|
||||||
|
- Sicherungskopien: rollierend bis zu 14 Tage; danach Überschreibung. Bei Wiederherstellung werden zuvor fällige Löschungen erneut ausgeführt.
|
||||||
|
|
||||||
|
Interne Kaffee- oder Getränkebuchungen des Kunden werden nicht pauschal als steuerliche Belege des Anbieters behandelt. Der Kunde bestimmt im Rahmen seiner Verantwortung eigene Aufbewahrungsfristen und kann den Mandanten löschen. Gesetzlich aufzubewahrende Abrechnungsdaten des Anbieters bleiben getrennt erhalten.
|
||||||
|
|
||||||
|
## 12. Ihre Rechte
|
||||||
|
|
||||||
|
Soweit CTB-IT Verantwortlicher ist, bestehen nach den gesetzlichen Voraussetzungen Rechte auf Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit und Widerspruch. Eine erteilte Einwilligung in die Nutzungsanalyse kann jederzeit über „Cookie-Einstellungen“ mit Wirkung für die Zukunft widerrufen werden.
|
||||||
|
|
||||||
|
Gegen Verarbeitungen nach Art. 6 Abs. 1 lit. f DSGVO können Sie aus Gründen Ihrer besonderen Situation widersprechen. Bei Direktwerbung wäre ein Widerspruch jederzeit ohne Begründung möglich; Direktwerbung ist derzeit nicht vorgesehen.
|
||||||
|
|
||||||
|
Anfragen richten Sie an info@ctb-it.de oder über das Ticketsystem im Impressum. Sie können sich außerdem bei einer Datenschutzaufsichtsbehörde beschweren. Für den Anbieter ist insbesondere die Landesbeauftragte für den Datenschutz Niedersachsen, Prinzenstraße 5, 30159 Hannover, zuständig.
|
||||||
|
|
||||||
|
## 13. Automatisierte Entscheidungen
|
||||||
|
|
||||||
|
Es findet keine ausschließlich automatisierte Entscheidungsfindung einschließlich Profiling statt, die rechtliche Wirkung entfaltet oder Sie ähnlich erheblich beeinträchtigt. Sicherheitslimits können Anfragen kurzfristig technisch blockieren; eine Vertragsentscheidung wird damit nicht getroffen.
|
||||||
|
|
||||||
|
## 14. Änderungen
|
||||||
|
|
||||||
|
Wir aktualisieren diese Erklärung, wenn sich Verarbeitung oder Rechtslage ändern. Bei wesentlichen Änderungen für bestehende Kunden informieren wir über einen geeigneten Kontaktweg.
|
||||||
|
|
||||||
|
Stand: 27. August 2026
|
||||||
@@ -69,6 +69,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -134,6 +135,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -117,6 +118,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -112,6 +113,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
|
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -209,6 +210,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
<a class="legal-action-link" href="kuendigen.php">Verträge hier kündigen</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../app/analytics.php';
|
||||||
|
require_once __DIR__ . '/../app/legal.php';
|
||||||
|
|
||||||
|
$failures = 0;
|
||||||
|
$assert = static function (string $label, bool $condition) use (&$failures): void {
|
||||||
|
if ($condition) {
|
||||||
|
echo "OK: {$label}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$failures++;
|
||||||
|
echo "FEHLER: {$label}\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
$environmentNames = [
|
||||||
|
'APP_ENV',
|
||||||
|
'APP_HOST',
|
||||||
|
'APP_MARKETING_URL',
|
||||||
|
'MATOMO_URL',
|
||||||
|
'MATOMO_SITE_ID',
|
||||||
|
'MATOMO_CONSENT_COOKIE_DOMAIN',
|
||||||
|
'MATOMO_PRIVACY_CONFIRMED',
|
||||||
|
];
|
||||||
|
$previousEnvironment = [];
|
||||||
|
foreach ($environmentNames as $name) {
|
||||||
|
$previousEnvironment[$name] = getenv($name);
|
||||||
|
putenv($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
putenv('APP_ENV=dev');
|
||||||
|
$assert('Ohne Konfiguration ist Matomo deaktiviert', app_matomo_configuration() === null);
|
||||||
|
$assert('Ohne Konfiguration wird kein Banner ausgegeben', app_analytics_body_html() === '');
|
||||||
|
|
||||||
|
putenv('MATOMO_URL=https://analytics.example.test/');
|
||||||
|
putenv('MATOMO_SITE_ID=ungueltig');
|
||||||
|
putenv('MATOMO_PRIVACY_CONFIRMED=1');
|
||||||
|
$assert('Ungültige Site-ID wird abgelehnt', app_matomo_configuration_errors() !== []);
|
||||||
|
$assert('Fehlerhafte Konfiguration lädt kein Matomo', app_matomo_configuration() === null);
|
||||||
|
|
||||||
|
putenv('MATOMO_SITE_ID=7');
|
||||||
|
putenv('MATOMO_CONSENT_COOKIE_DOMAIN=.example.test');
|
||||||
|
$assert('HTTP wird in Produktion für Matomo abgelehnt', (static function (): bool {
|
||||||
|
putenv('APP_ENV=prod');
|
||||||
|
putenv('MATOMO_URL=http://analytics.example.test/');
|
||||||
|
$hasError = app_matomo_configuration_errors() !== [];
|
||||||
|
putenv('APP_ENV=dev');
|
||||||
|
putenv('MATOMO_URL=https://analytics.example.test/');
|
||||||
|
return $hasError;
|
||||||
|
})());
|
||||||
|
$configuration = app_matomo_configuration();
|
||||||
|
$assert('Vollständige Konfiguration wird aktiviert', $configuration !== null);
|
||||||
|
$assert('Matomo-Basis-URL wird normalisiert', ($configuration['matomoUrl'] ?? '') === 'https://analytics.example.test/');
|
||||||
|
$assert('Einwilligung gilt 180 Tage', ($configuration['consentMaxAge'] ?? 0) === 15552000);
|
||||||
|
|
||||||
|
$head = app_analytics_head_html();
|
||||||
|
$body = app_analytics_body_html();
|
||||||
|
$assert('Consent-Stylesheet wird lokal geladen', str_contains($head, 'assets/css/analytics-consent.css'));
|
||||||
|
$assert('Zustimmen und Ablehnen stehen in derselben Ebene',
|
||||||
|
str_contains($body, 'data-analytics-consent="denied"')
|
||||||
|
&& str_contains($body, 'data-analytics-consent="granted"'));
|
||||||
|
$assert('Matomo selbst ist im HTML nicht vorab eingebunden',
|
||||||
|
!str_contains($body, 'src="https://analytics.example.test'));
|
||||||
|
$assert('Cookie-Einstellungen bleiben nach der Entscheidung erreichbar',
|
||||||
|
str_contains($body, 'analytics-consent-settings'));
|
||||||
|
|
||||||
|
$javascript = file_get_contents(APP_ROOT . '/assets/js/analytics-consent.js') ?: '';
|
||||||
|
$assert('Tracker verlangt Tracking-Einwilligung', str_contains($javascript, "['requireConsent']"));
|
||||||
|
$assert('Matomo-Script wird erst dynamisch nach Zustimmung erzeugt',
|
||||||
|
str_contains($javascript, "document.createElement('script')"));
|
||||||
|
$assert('Query und Fragment werden aus Analyse-URLs entfernt',
|
||||||
|
str_contains($javascript, 'url.origin + url.pathname'));
|
||||||
|
$assert('Zusätzliche Browser- und Linkmessung bleibt deaktiviert',
|
||||||
|
str_contains($javascript, "['disableBrowserFeatureDetection']")
|
||||||
|
&& str_contains($javascript, "['disablePerformanceTracking']")
|
||||||
|
&& !str_contains($javascript, "['enableLinkTracking']"));
|
||||||
|
$assert('Widerruf stoppt Tracking und löscht Cookies',
|
||||||
|
str_contains($javascript, "['forgetConsentGiven']")
|
||||||
|
&& str_contains($javascript, "['deleteCookies']"));
|
||||||
|
|
||||||
|
$publicTemplates = [
|
||||||
|
'abo-bestellen.php',
|
||||||
|
'agb.php',
|
||||||
|
'avv.php',
|
||||||
|
'datenschutz.php',
|
||||||
|
'email-verifizieren.php',
|
||||||
|
'impressum.php',
|
||||||
|
'kuendigen.php',
|
||||||
|
'landing.php',
|
||||||
|
'login.php',
|
||||||
|
'passwort-vergessen.php',
|
||||||
|
'passwort-zuruecksetzen.php',
|
||||||
|
'preise.php',
|
||||||
|
'register.php',
|
||||||
|
'widerruf.php',
|
||||||
|
'widerrufen.php',
|
||||||
|
'zwei-faktor.php',
|
||||||
|
];
|
||||||
|
foreach ($publicTemplates as $template) {
|
||||||
|
$contents = file_get_contents(APP_ROOT . '/' . $template) ?: '';
|
||||||
|
$assert("{$template} bindet die Einwilligung vollständig ein",
|
||||||
|
str_contains($contents, 'app_analytics_head_html')
|
||||||
|
&& str_contains($contents, 'app_analytics_body_html'));
|
||||||
|
}
|
||||||
|
$assert('App-Layout bindet Head und Body zentral ein',
|
||||||
|
str_contains(file_get_contents(APP_ROOT . '/header.php') ?: '', 'app_analytics_head_html')
|
||||||
|
&& str_contains(file_get_contents(APP_ROOT . '/footer.php') ?: '', 'app_analytics_body_html'));
|
||||||
|
|
||||||
|
$privacy = app_legal_document_text('privacy_notice');
|
||||||
|
$assert('Aktuelle Datenschutzerklärung beschreibt Matomo und Widerruf',
|
||||||
|
str_contains($privacy, 'Matomo')
|
||||||
|
&& str_contains($privacy, 'Cookie-Einstellungen')
|
||||||
|
&& str_contains($privacy, 'Verifizierungs-, Passwort-Reset- und Zugriffs-Tokens'));
|
||||||
|
|
||||||
|
foreach ($previousEnvironment as $name => $value) {
|
||||||
|
$value === false ? putenv($name) : putenv($name . '=' . $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failures > 0) {
|
||||||
|
echo "\n{$failures} Analyse-/Consent-Prüfung(en) fehlgeschlagen.\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nAnalyse und Einwilligung: alle Prüfungen bestanden.\n";
|
||||||
@@ -25,6 +25,10 @@ if ($baseUrl !== '' && (!str_starts_with($baseUrl, 'https://') || parse_url($bas
|
|||||||
$errors[] = 'APP_BASE_URL muss eine HTTPS-URL auf APP_HOST sein.';
|
$errors[] = 'APP_BASE_URL muss eine HTTPS-URL auf APP_HOST sein.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (app_matomo_configuration_errors() as $matomoError) {
|
||||||
|
$errors[] = $matomoError;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$dbTablePrefix = app_db_table_prefix();
|
$dbTablePrefix = app_db_table_prefix();
|
||||||
if ($dbTablePrefix === '') {
|
if ($dbTablePrefix === '') {
|
||||||
@@ -73,6 +77,13 @@ if (preg_match('~@import\s+url\(["\']?https?://|url\(["\']?https?://~i', $mainCs
|
|||||||
$errors[] = 'assets/css/main.css lädt noch externe Ressourcen.';
|
$errors[] = 'assets/css/main.css lädt noch externe Ressourcen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$privacyText = app_legal_document_text('privacy_notice');
|
||||||
|
foreach (['Matomo', 'kaffeeliste_analytics_consent', 'Cookie-Einstellungen'] as $privacyRequirement) {
|
||||||
|
if (!str_contains($privacyText, $privacyRequirement)) {
|
||||||
|
$errors[] = "Datenschutzerklärung nennt {$privacyRequirement} nicht.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
foreach (['legal_acceptances', 'legal_requests', 'tenant_billing', 'rate_limit_attempts'] as $table) {
|
foreach (['legal_acceptances', 'legal_requests', 'tenant_billing', 'rate_limit_attempts'] as $table) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -25,6 +26,7 @@ require_once __DIR__ . '/app/legal.php';
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<link rel="stylesheet" href="assets/css/public.css">
|
<link rel="stylesheet" href="assets/css/public.css">
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -112,6 +113,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<?php echo app_public_legal_footer(); ?>
|
<?php echo app_public_legal_footer(); ?>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
<script src="assets/js/jquery.min.js"></script>
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
<script src="assets/js/browser.min.js"></script>
|
<script src="assets/js/browser.min.js"></script>
|
||||||
<script src="assets/js/breakpoints.min.js"></script>
|
<script src="assets/js/breakpoints.min.js"></script>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="assets/css/main.css" />
|
<link rel="stylesheet" href="assets/css/main.css" />
|
||||||
<link rel="stylesheet" href="assets/css/public.css" />
|
<link rel="stylesheet" href="assets/css/public.css" />
|
||||||
|
<?php echo app_analytics_head_html(); ?>
|
||||||
</head>
|
</head>
|
||||||
<body class="is-preload public-page">
|
<body class="is-preload public-page">
|
||||||
<div class="public-shell">
|
<div class="public-shell">
|
||||||
@@ -112,5 +113,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<a href="avv.php">AVV</a>
|
<a href="avv.php">AVV</a>
|
||||||
<a href="widerruf.php">Widerrufsbelehrung</a>
|
<a href="widerruf.php">Widerrufsbelehrung</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
<?php echo app_analytics_body_html(); ?>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user