177 lines
7.5 KiB
PHP
177 lines
7.5 KiB
PHP
<?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);
|
|
$assert('Neue Tracking-Art fragt bestehende Entscheidungen erneut ab', ($configuration['consentVersion'] ?? '') === '2');
|
|
|
|
$head = app_analytics_head_html();
|
|
$body = app_analytics_body_html();
|
|
$assert('Consent-Stylesheet wird lokal und filterarm geladen', str_contains($head, 'assets/css/privacy-settings.css'));
|
|
$assert('Zustimmen und Ablehnen stehen in derselben Ebene',
|
|
str_contains($body, 'data-analytics-consent="denied">Nur notwendige Cookies</button>')
|
|
&& str_contains($body, 'data-analytics-consent="granted">Zustimmen</button>'));
|
|
$assert('Einwilligungsdialog ist eindeutig benannt',
|
|
str_contains($body, '<h2 id="analytics-consent-title">Cookie-Zustimmung</h2>'));
|
|
$consentCss = file_get_contents(APP_ROOT . '/assets/css/privacy-settings.css') ?: '';
|
|
$assert('Nur notwendige Cookies ist innen weiß gestaltet',
|
|
str_contains($consentCss, '[data-analytics-consent="denied"]')
|
|
&& str_contains($consentCss, 'background: #ffffff;'));
|
|
$assert('Matomo selbst ist im HTML nicht vorab eingebunden',
|
|
!str_contains($body, 'https://analytics.example.test')
|
|
&& !str_contains($body, '"siteId"'));
|
|
$assert('Kein schwebender Cookie-Schalter wird mehr ausgegeben',
|
|
!str_contains($body, 'analytics-consent-settings'));
|
|
$assert('Footer-Schalter wird bei aktiver Analyse ausgegeben',
|
|
str_contains(app_analytics_footer_settings_button_html(), 'data-analytics-settings')
|
|
&& str_contains(app_public_legal_footer(), 'Cookie-Einstellungen'));
|
|
$assert('Browser nutzt den lokalen First-Party-Endpunkt',
|
|
str_contains($body, '"trackingEndpoint":"nutzungsanalyse.php"')
|
|
&& str_contains($body, 'assets/js/privacy-settings.js'));
|
|
|
|
$javascript = file_get_contents(APP_ROOT . '/assets/js/privacy-settings.js') ?: '';
|
|
$assert('Tracker sendet nur bei erteilter Einwilligung',
|
|
str_contains($javascript, "readDecision() !== 'granted'"));
|
|
$assert('Browser sendet nur an den lokalen Endpunkt',
|
|
str_contains($javascript, 'window.fetch(configuration.trackingEndpoint')
|
|
&& !str_contains($javascript, 'matomo.js')
|
|
&& !str_contains($javascript, 'matomo.php'));
|
|
$assert('Query und Fragment werden aus Analyse-URLs entfernt',
|
|
str_contains($javascript, 'url.origin + url.pathname'));
|
|
$assert('Keine Kaffeeliste-Benutzerkennung wird gelesen',
|
|
!str_contains($javascript, 'userId')
|
|
&& !str_contains($javascript, 'sessionStorage')
|
|
&& !str_contains($javascript, 'localStorage'));
|
|
$assert('Widerruf stoppt Tracking und löscht Analyse-Cookies',
|
|
str_contains($javascript, 'removeAnalysisCookies()')
|
|
&& str_contains($javascript, "name !== visitorCookieName"));
|
|
$assert('Alle Cookie-Einstellungs-Schalter öffnen denselben Dialog',
|
|
str_contains($javascript, "querySelectorAll('[data-analytics-settings]')")
|
|
&& str_contains($javascript, 'settingsButtons.forEach'));
|
|
|
|
$relay = file_get_contents(APP_ROOT . '/app/analytics-relay.php') ?: '';
|
|
$assert('Relay übernimmt keine Client-IP',
|
|
!str_contains($relay, 'REMOTE_ADDR')
|
|
&& !str_contains($relay, "'cip'"));
|
|
$assert('Relay bindet Ziel und Site-ID an die Serverkonfiguration',
|
|
str_contains($relay, "['idsite'] = \$configuration['siteId']")
|
|
&& str_contains($relay, "\$configuration['matomoUrl'] . 'matomo.php'"));
|
|
|
|
$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'));
|
|
$appFooter = file_get_contents(APP_ROOT . '/footer.php') ?: '';
|
|
$assert('App-Footer enthält Cookie-Einstellungen ohne Copyright- und Designlink',
|
|
str_contains($appFooter, 'app_analytics_footer_settings_button_html')
|
|
&& !str_contains($appFooter, '© Clemens Creutzburg')
|
|
&& !str_contains($appFooter, 'html5up.net'));
|
|
|
|
foreach ([
|
|
'email-verifizieren.php',
|
|
'landing.php',
|
|
'login.php',
|
|
'passwort-vergessen.php',
|
|
'passwort-zuruecksetzen.php',
|
|
'register.php',
|
|
'zwei-faktor.php',
|
|
] as $manualFooterTemplate) {
|
|
$contents = file_get_contents(APP_ROOT . '/' . $manualFooterTemplate) ?: '';
|
|
$assert("{$manualFooterTemplate} bietet Cookie-Einstellungen im Footer an",
|
|
str_contains($contents, 'app_analytics_footer_settings_button_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, 'kaffeeliste_analytics_visitor')
|
|
&& str_contains($privacy, 'nicht an Matomo weitergegeben')
|
|
&& 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";
|