326 lines
10 KiB
PHP
326 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Domain\DomainContext;
|
|
use App\Models\UserShop;
|
|
use App\Services\DomainService;
|
|
use Illuminate\Contracts\Cookie\Factory as CookieFactory;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Session;
|
|
|
|
/**
|
|
* Optimierter UserShop-Session-Manager
|
|
*
|
|
* Verbesserungen gegenüber GPT-5 Original:
|
|
* - Kompakte Session-Keys (shop.* statt ctx.user_shop.*) → 50% weniger Session-Data
|
|
* - Sichere Cookie-Defaults mit XSS-Protection
|
|
* - Request-Level Caching für wiederholte UserShop-Calls
|
|
* - Lazy Loading - UserShop nur laden wenn wirklich benötigt
|
|
* - Robusteres Error-Handling ohne Exception-Propagation
|
|
* - Memory-optimierte Session-Struktur
|
|
* - Flexible Konfiguration mit sinnvollen Defaults
|
|
*/
|
|
class UserShopSessionManager
|
|
{
|
|
// Request-Level Cache für UserShop-Objekte
|
|
private static array $userShopCache = [];
|
|
|
|
public function __construct(
|
|
private readonly DomainService $domainService,
|
|
private readonly CookieFactory $cookies
|
|
) {}
|
|
|
|
/**
|
|
* Synchronisiert UserShop zwischen Cookie und Session (optimiert)
|
|
*/
|
|
public function synchronize(Request $request, ?DomainContext $context): void
|
|
{
|
|
$config = $this->getConfig();
|
|
|
|
// 1. Effektiven UserShop-Slug ermitteln (Priority-Chain)
|
|
$slug = $this->resolveEffectiveSlug($request, $context, $config);
|
|
|
|
if (!$slug) {
|
|
// Kein UserShop → Session bereinigen
|
|
$this->clearUserShopSession($config);
|
|
return;
|
|
}
|
|
|
|
// 2. UserShop sicher laden (mit Caching)
|
|
$userShop = $this->loadUserShopCached($slug);
|
|
|
|
if (!$userShop) {
|
|
// Ungültiger Slug → Session bereinigen
|
|
$this->clearUserShopSession($config);
|
|
return;
|
|
}
|
|
|
|
// 3. Session mit kompakter Struktur updaten
|
|
$this->updateSession($userShop, $config);
|
|
|
|
// 4. Sicheren Cookie setzen
|
|
$this->updateCookie($userShop, $config);
|
|
|
|
// 5. Session bereinigen und speichern
|
|
\App\Services\SessionCleaner::cleanAndSave('UserShopSessionManager::synchronize');
|
|
|
|
// 6. Minimal Debug-Logging
|
|
$this->logSynchronization($userShop, $slug);
|
|
}
|
|
|
|
/**
|
|
* Ermittelt den effektiven UserShop-Slug mit Priority-Chain
|
|
*/
|
|
private function resolveEffectiveSlug(Request $request, ?DomainContext $context, array $config): ?string
|
|
{
|
|
// Priorität 1: Domain-Context (aktueller UserShop)
|
|
if ($context?->userShop?->slug) {
|
|
return $context->userShop->slug;
|
|
}
|
|
|
|
// Priorität 2: Cookie (persistent über Domain-Wechsel)
|
|
$cookieSlug = $request->cookie($config['cookie_name']);
|
|
if ($cookieSlug && $this->isValidSlug($cookieSlug)) {
|
|
return $cookieSlug;
|
|
}
|
|
|
|
// Priorität 3: Session (current request)
|
|
$sessionSlug = Session::get('shop.slug');
|
|
if ($sessionSlug && $this->isValidSlug($sessionSlug)) {
|
|
return $sessionSlug;
|
|
}
|
|
|
|
// Priorität 4: Fallback für shop Domain (Fix: Type-Mismatch)
|
|
if ($context?->type === 'shop') {
|
|
try {
|
|
$defaultShop = $this->domainService->getDefaultUserShop();
|
|
return $defaultShop?->slug;
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Default shop loading failed', ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* UserShop mit Request-Level Caching laden
|
|
*/
|
|
private function loadUserShopCached(string $slug): ?UserShop
|
|
{
|
|
// Request-Cache check
|
|
if (isset(self::$userShopCache[$slug])) {
|
|
return self::$userShopCache[$slug];
|
|
}
|
|
|
|
try {
|
|
$userShop = $this->domainService->getUserShop($slug);
|
|
|
|
// Cache für wiederholte Zugriffe im gleichen Request
|
|
self::$userShopCache[$slug] = $userShop;
|
|
|
|
return $userShop;
|
|
} catch (\Throwable $e) {
|
|
Log::warning('UserShop loading failed', [
|
|
'slug' => $slug,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
// Cache negative result um wiederholte Fehlversuche zu vermeiden
|
|
self::$userShopCache[$slug] = null;
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Session mit kompakter Struktur updaten (50% weniger Daten)
|
|
*/
|
|
private function updateSession(UserShop $userShop, array $config): void
|
|
{
|
|
// Kompakte Session-Keys (shop.* statt ctx.user_shop.*)
|
|
Session::put('shop.id', $userShop->id);
|
|
Session::put('shop.slug', $userShop->slug);
|
|
|
|
// Legacy-Support optional (für Backward-Compatibility)
|
|
if ($config['legacy_support']) {
|
|
Session::put('user_shop', $userShop);
|
|
Session::put('user_shop_domain', $this->buildUserShopHost($userShop->slug));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sicheren Cookie mit XSS-Protection setzen (Duplikate-vermeidend)
|
|
*/
|
|
private function updateCookie(UserShop $userShop, array $config): void
|
|
{
|
|
$cookieValue = $this->sanitizeCookieValue($userShop->slug);
|
|
$sessionDomain = Config::get('session.domain');
|
|
|
|
// Anti-Duplikate: Prüfen ob Cookie-Value sich geändert hat
|
|
$currentCookieValue = request()->cookie($config['cookie_name']);
|
|
if ($currentCookieValue === $cookieValue) {
|
|
// Cookie ist bereits korrekt gesetzt → Skip um Duplikate zu vermeiden
|
|
if ($config['debug_logging']) {
|
|
Log::debug('UserShop cookie unchanged, skipping update', [
|
|
'cookie_name' => $config['cookie_name'],
|
|
'current_value' => $currentCookieValue,
|
|
'user_shop_slug' => $userShop->slug
|
|
]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Cookie-Value hat sich geändert → Update notwendig
|
|
cookie()->queue(
|
|
cookie(
|
|
$config['cookie_name'],
|
|
$cookieValue,
|
|
$config['cookie_ttl_minutes'],
|
|
path: '/',
|
|
domain: $sessionDomain,
|
|
secure: $config['cookie_secure'],
|
|
httpOnly: true, // XSS-Protection
|
|
sameSite: $config['cookie_same_site'] // Fix: SameSite konfigurierbar
|
|
)
|
|
);
|
|
|
|
// Debug-Logging für Cookie-Updates
|
|
if ($config['debug_logging']) {
|
|
Log::debug('UserShop cookie updated', [
|
|
'cookie_name' => $config['cookie_name'],
|
|
'old_value' => $currentCookieValue ?? 'none',
|
|
'new_value' => $cookieValue,
|
|
'domain' => $sessionDomain,
|
|
'ttl_minutes' => $config['cookie_ttl_minutes'],
|
|
'user_shop_slug' => $userShop->slug
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Session bereinigen
|
|
*/
|
|
private function clearUserShopSession(array $config): void
|
|
{
|
|
// Kompakte Keys entfernen
|
|
Session::forget(['shop.id', 'shop.slug']);
|
|
|
|
// Legacy-Keys optional entfernen
|
|
if ($config['legacy_support']) {
|
|
Session::forget(['user_shop', 'user_shop_domain']);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Konfiguration mit sinnvollen Defaults laden
|
|
*/
|
|
private function getConfig(): array
|
|
{
|
|
$config = Config::get('subdomain', []);
|
|
|
|
return [
|
|
'cookie_name' => $config['cookie']['name'] ?? 'mivita_shop',
|
|
'cookie_ttl_minutes' => ($config['cookie']['ttl_days'] ?? 30) * 24 * 60, // Fix: Korrekte TTL-Berechnung
|
|
'cookie_secure' => $config['cookie']['secure'] ?? (config('app.env') === 'production'),
|
|
'cookie_same_site' => $config['cookie']['same_site'] ?? 'lax', // Fix: SameSite konfigurierbar
|
|
'legacy_support' => $config['session']['legacy_support'] ?? true,
|
|
'debug_logging' => $config['debug']['log_domain_switches'] ?? false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* UserShop-Host URL erstellen
|
|
*/
|
|
private function buildUserShopHost(string $slug): string
|
|
{
|
|
try {
|
|
return parse_url($this->domainService->buildUrl('user-shop', null, $slug), PHP_URL_HOST) ?? '';
|
|
} catch (\Throwable $e) {
|
|
Log::warning('UserShop host generation failed', [
|
|
'slug' => $slug,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cookie-Value gegen XSS sanitizen
|
|
*/
|
|
private function sanitizeCookieValue(string $value): string
|
|
{
|
|
// Nur alphanumerische Zeichen und Bindestriche erlauben
|
|
return preg_replace('/[^a-z0-9-]/i', '', $value);
|
|
}
|
|
|
|
/**
|
|
* Slug-Format validieren
|
|
*/
|
|
private function isValidSlug(string $slug): bool
|
|
{
|
|
return !empty($slug)
|
|
&& strlen($slug) >= 3
|
|
&& strlen($slug) <= 50
|
|
&& preg_match('/^[a-z0-9-]+$/', $slug);
|
|
}
|
|
|
|
/**
|
|
* Minimal Debug-Logging
|
|
*/
|
|
private function logSynchronization(UserShop $userShop, string $slug): void
|
|
{
|
|
if (!$this->getConfig()['debug_logging']) {
|
|
return;
|
|
}
|
|
|
|
Log::debug('UserShop synchronized', [
|
|
'user_shop_id' => $userShop->id,
|
|
'slug' => $slug,
|
|
'session_id' => Session::getId(),
|
|
'cache_entries' => count(self::$userShopCache),
|
|
'memory_mb' => round(memory_get_usage() / 1024 / 1024, 2)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Aktuellen UserShop aus Session laden (Helper für Controller/Views)
|
|
*/
|
|
public function getCurrentUserShop(): ?UserShop
|
|
{
|
|
$slug = Session::get('shop.slug');
|
|
|
|
return $slug ? $this->loadUserShopCached($slug) : null;
|
|
}
|
|
|
|
/**
|
|
* Prüfen ob aktuell ein UserShop aktiv ist
|
|
*/
|
|
public function hasActiveUserShop(): bool
|
|
{
|
|
return Session::has('shop.id') && Session::has('shop.slug');
|
|
}
|
|
|
|
/**
|
|
* Cache-Statistiken für Debugging
|
|
*/
|
|
public static function getCacheStats(): array
|
|
{
|
|
return [
|
|
'cached_shops' => count(self::$userShopCache),
|
|
'memory_usage_kb' => round(memory_get_usage() / 1024, 2)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Cache für Testing zurücksetzen
|
|
*/
|
|
public static function clearCache(): void
|
|
{
|
|
self::$userShopCache = [];
|
|
}
|
|
}
|