<?php declare(strict_types=1);
namespace Asbs\DynamicAccessRedirect\Subscriber;
use Shopware\Core\Content\Category\Exception\CategoryNotFoundException;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
class DynamicAccessSubscriber implements EventSubscriberInterface
{
private EntityRepositoryInterface $categoryRepository;
private EntityRepositoryInterface $productRepository;
private RouterInterface $router;
private SystemConfigService $systemConfigService;
/**
* Default redirect URL for fallback
*/
private const DEFAULT_REDIRECT_URL = 'https://doncarne.de/c/beef-club/';
public function __construct(
EntityRepositoryInterface $categoryRepository,
EntityRepositoryInterface $productRepository,
RouterInterface $router,
SystemConfigService $systemConfigService
) {
$this->categoryRepository = $categoryRepository;
$this->productRepository = $productRepository;
$this->router = $router;
$this->systemConfigService = $systemConfigService;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['onKernelException', 0]
];
}
public function onKernelException(ExceptionEvent $event): void
{
// Überprüfen, ob das Plugin aktiviert ist
if (!$this->isPluginEnabled()) {
return;
}
$exception = $event->getThrowable();
$request = $event->getRequest();
// Überprüfen, ob ein sales channel context und ein eingeloggter Benutzer vorhanden ist
if (!$request->attributes->has('sw-sales-channel-context')) {
return;
}
/** @var SalesChannelContext $context */
$context = $request->attributes->get('sw-sales-channel-context');
// Nur 404 Exceptions, CategoryNotFoundException und ProductNotFoundException behandeln
if (!($exception instanceof NotFoundHttpException) &&
!($exception instanceof CategoryNotFoundException) &&
!($exception instanceof ProductNotFoundException)) {
return;
}
// URL-Pfadmuster überprüfen, die durch Dynamic Access eingeschränkt sein könnten
$pathInfo = $request->getPathInfo();
// Kategorie-Exceptions behandeln - prüfen, ob die Kategorie tatsächlich existiert
if (($exception instanceof CategoryNotFoundException) || $this->isCategoryPath($pathInfo)) {
$categoryId = $this->extractCategoryIdFromPath($pathInfo);
if ($categoryId && $this->categoryExists($categoryId, $context)) {
// Kategorie existiert, aber Benutzer hat keinen Zugriff, zum Beef Club weiterleiten
$response = new RedirectResponse($this->getRedirectUrl());
$event->setResponse($response);
return;
}
}
// Produkt-Exceptions behandeln - prüfen, ob das Produkt tatsächlich existiert
if (($exception instanceof ProductNotFoundException) || $this->isProductPath($pathInfo)) {
$productId = $this->extractProductIdFromPath($pathInfo);
if ($productId && $this->productExists($productId, $context)) {
// Produkt existiert, aber Benutzer hat keinen Zugriff, zum Beef Club weiterleiten
$response = new RedirectResponse($this->getRedirectUrl());
$event->setResponse($response);
return;
}
}
}
/**
* Bestimmt, ob der Pfad wie eine Kategorieseite aussieht
*/
private function isCategoryPath(string $pathInfo): bool
{
// Kategoriepfade haben typischerweise keine Erweiterungen und enthalten nicht /detail/ oder /checkout/
return !preg_match('/(\.[\w\d]+$|\/detail\/|\/checkout\/)/', $pathInfo);
}
/**
* Bestimmt, ob der Pfad wie eine Produktdetailseite aussieht
*/
private function isProductPath(string $pathInfo): bool
{
// Produktpfade enthalten typischerweise /detail/
return strpos($pathInfo, '/detail/') !== false;
}
/**
* Extrahiert eine mögliche Kategorie-ID aus einem URL-Pfad
*/
private function extractCategoryIdFromPath(string $pathInfo): ?string
{
// Den letzten Teil der URL extrahieren, der die Kategorie-ID sein sollte
if (preg_match('/\/([a-f0-9]{32})(?:\/|$)/', $pathInfo, $matches)) {
return $matches[1];
}
return null;
}
/**
* Extrahiert eine mögliche Produkt-ID aus einem URL-Pfad
*/
private function extractProductIdFromPath(string $pathInfo): ?string
{
// Die ID aus einem Pfad wie '/detail/a1b2c3.../' extrahieren
if (preg_match('/\/detail\/([a-f0-9]{32})(?:\/|$)/', $pathInfo, $matches)) {
return $matches[1];
}
return null;
}
/**
* Überprüft, ob eine Kategorie in der Datenbank existiert, ohne Kundengruppen-Filter anzuwenden
*/
private function categoryExists(string $categoryId, SalesChannelContext $context): bool
{
$criteria = new Criteria([$categoryId]);
$criteria->addFilter(new EqualsFilter('active', true));
$criteria->addAssociation('type');
// Direkter Zugriff auf das Repository, um Sales-Channel-Filter zu umgehen
$result = $this->categoryRepository->search($criteria, $context->getContext());
return $result->getTotal() > 0;
}
/**
* Überprüft, ob ein Produkt in der Datenbank existiert, ohne Kundengruppen-Filter anzuwenden
*/
private function productExists(string $productId, SalesChannelContext $context): bool
{
$criteria = new Criteria([$productId]);
$criteria->addFilter(new EqualsFilter('active', true));
// Direkter Zugriff auf das Repository, um Sales-Channel-Filter zu umgehen
$result = $this->productRepository->search($criteria, $context->getContext());
return $result->getTotal() > 0;
}
/**
* Gibt die konfigurierte Weiterleitungs-URL zurück
*/
private function getRedirectUrl(): string
{
// URL aus den Einstellungen holen und Whitespace trimmen
$redirectUrl = trim($this->systemConfigService->getString(
'AsbsDynamicAccessRedirect.config.redirectUrl',
null,
''
));
// Fallback zur Standard-URL, wenn keine konfiguriert ist
if (empty($redirectUrl)) {
return self::DEFAULT_REDIRECT_URL;
}
// Prüfen, ob der Trailing Slash beibehalten werden soll
$preserveTrailingSlash = $this->systemConfigService->getBool(
'AsbsDynamicAccessRedirect.config.preserveTrailingSlash',
null,
true
);
// Redirect-URL-Zeichenkette ohne Änderungen extrahieren
$originalUrl = (string) $redirectUrl;
// Überprüfen, ob die ursprüngliche URL mit "/" endet
$endsWithSlash = substr($originalUrl, -1) === '/';
if ($preserveTrailingSlash && $endsWithSlash) {
// Sicherstellen, dass die URL mit einem Slash endet, indem wir ihn explizit hinzufügen
// Zuerst alle Slashes am Ende entfernen und dann einen hinzufügen
return rtrim($originalUrl, '/') . '/';
} elseif (!$preserveTrailingSlash) {
// Slash entfernen, wenn nicht gewünscht
return rtrim($originalUrl, '/');
}
// Wenn kein Slash am Ende war oder Option nicht aktiviert, URL unverändert zurückgeben
return $originalUrl;
}
/**
* Überprüft, ob das Plugin aktiviert ist
*/
private function isPluginEnabled(): bool
{
// Der zweite Parameter sollte die salesChannelId sein (null für system-global)
// Der dritte Parameter ist der Standardwert, falls die Konfiguration nicht gefunden wird
return $this->systemConfigService->getBool(
'AsbsDynamicAccessRedirect.config.enabled',
null,
true
);
}
}