Files

171 lines
6.7 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
const APP_ANALYTICS_CONSENT_COOKIE = 'kaffeeliste_analytics_consent';
const APP_ANALYTICS_VISITOR_COOKIE = 'kaffeeliste_analytics_visitor';
const APP_ANALYTICS_CONSENT_VERSION = '2';
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, visitorCookie: 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,
'visitorCookie' => APP_ANALYTICS_VISITOR_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/privacy-settings.css">' . "\n";
}
function app_analytics_footer_settings_button_html(): string
{
if (app_matomo_configuration() === null) {
return '';
}
return '<button type="button" class="footer-cookie-settings" data-analytics-settings>'
. 'Cookie-Einstellungen</button>';
}
function app_analytics_body_html(): string
{
$configuration = app_matomo_configuration();
if ($configuration === null) {
return '';
}
// Matomo-Host und Site-ID bleiben serverseitig. Der Browser sendet nach
// Einwilligung nur an den First-Party-Endpunkt; dieser leitet die eng
// validierte Seitenansicht an die fest konfigurierte Matomo-Site weiter.
$browserConfiguration = [
'consentCookie' => $configuration['consentCookie'],
'visitorCookie' => $configuration['visitorCookie'],
'consentVersion' => $configuration['consentVersion'],
'consentMaxAge' => $configuration['consentMaxAge'],
'cookieDomain' => $configuration['cookieDomain'],
'secureCookies' => $configuration['secureCookies'],
'trackingEndpoint' => 'nutzungsanalyse.php',
];
$json = json_encode(
$browserConfiguration,
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">Cookie-Zustimmung</h2>
<p id="analytics-consent-description">Wir möchten mit Matomo verstehen, wie unsere Website und App genutzt werden. Erst nach deiner Zustimmung übermitteln wir Seitenaufrufe an unser selbst betriebenes Matomo und setzen ein Analyse-Cookie. 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 Cookies</button>
<button type="button" data-analytics-consent="granted">Zustimmen</button>
</div>
</div>
</section>
HTML
. "\n<script type=\"application/json\" id=\"analytics-configuration\">{$json}</script>\n"
. '<script src="assets/js/privacy-settings.js" defer></script>' . "\n";
}