Matomo-Tracking mit Einwilligung einbauen
This commit is contained in:
@@ -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();
|
||||
|
||||
// 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';
|
||||
|
||||
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_WITHDRAWAL_VERSION = '2026-08-22';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user