Matomo-Tracking mit Einwilligung einbauen

This commit is contained in:
2026-08-27 09:20:46 +02:00
parent a2dcc4df65
commit 528af07890
28 changed files with 793 additions and 2 deletions
+127
View File
@@ -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";
+11
View File
@@ -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.';
}
foreach (app_matomo_configuration_errors() as $matomoError) {
$errors[] = $matomoError;
}
try {
$dbTablePrefix = app_db_table_prefix();
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.';
}
$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 {
$pdo = app_db_pdo();
foreach (['legal_acceptances', 'legal_requests', 'tenant_billing', 'rate_limit_attempts'] as $table) {