<?php
namespace AbmAdjustments\Subscriber;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Event\BeforeSendRedirectResponseEvent;
use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Shopware\Core\Content\Cms\Exception\PageNotFoundException;
class ProductRedirectionSubscriber implements EventSubscriberInterface
{
private $router;
private $productRepository;
private $seoUrlRepository;
private $context;
private $requestStack;
private $connection;
private $slugify;
/**
* Undocumented function
*
* @param RouterInterface $router
* @param EntityRepositoryInterface $productRepository
*/
public function __construct(
RouterInterface $router,
EntityRepositoryInterface $productRepository,
RequestStack $requestStack,
Connection $connection,
EntityRepositoryInterface $seoUrlRepository
)
{
$this->router = $router;
$this->productRepository = $productRepository;
$this->context = Context::createDefaultContext();
$this->requestStack = $requestStack;
$this->connection = $connection;
$this->seoUrlRepository = $seoUrlRepository;
$this->slugify = new \Cocur\Slugify\Slugify();
}
/**
* @return string[]
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
BeforeSendRedirectResponseEvent::class => ['onBeforeSendRedirectResponseEvent', 1000001],
];
}
public function onBeforeSendRedirectResponseEvent(BeforeSendResponseEvent $event)
{
$request = $event->getRequest();
$pathInfo = $request->getPathInfo();
$statusCode = $event->getResponse()->getStatusCode();
if (strpos($pathInfo, 'detail') !== false && $statusCode == 301) {
$urlRedirect301 = str_replace($request->attributes->get('sw-storefront-url'), '', $request->attributes->get('sw-canonical-link'));
$productId = str_replace('/detail/', '', $pathInfo);
if(!empty($productId)){
$bResponse = $this->processRedirectProductDeleted($request, $event, $urlRedirect301, $productId);
if (empty($bResponse)) {
$aCandidateMatches = [];
$urlChunks = explode('/', str_replace('/a/', '', rtrim($request->attributes->get('sw-original-request-uri'), '/')));
$sSubject = $urlChunks[0];
$sFinalMatch = '';
foreach (array_values($this->getAllFrontNavigationRoutes()) as $aCandidateRoute) {
$sCandidateRoute = $aCandidateRoute['seo_path_info'];
$sMatch = $this->longestConsecutiveSubstring($sSubject, $sCandidateRoute);
if (!empty($sMatch)) {
$sFinalMatch = strlen($sMatch) > strlen($sFinalMatch) ? $sCandidateRoute : $sFinalMatch;
}
}
if (!empty($sFinalMatch)) {
$event->setResponse(new RedirectResponse($request->server->get('APP_URL') . '/' . $sFinalMatch, 301));
}
}
}
}
}
private function processRedirectProductDeleted($request, $event, $urlRedirect301 = '', $productId = '')
{
$product = $this->getProductById($productId);
//Product deactivaton block
if ($product) {
if (!$product->getActive()) {
$categoryRoute = $this->getCategoryRoute($productId);
if ($categoryRoute) {
$event->setResponse(new RedirectResponse($categoryRoute));
return true;
} else {
$homeLink = $this->router->generate('frontend.home.page', [], UrlGeneratorInterface::ABSOLUTE_URL);
$event->setResponse(new RedirectResponse($homeLink));
return true;
}
}
} else {
//Product deleted block
$url = $request->attributes->get('sw-original-request-uri');
//Case access url old of product
if (!empty($urlRedirect301)) {
$url = $urlRedirect301; //this is canonical url
}
if ($url) {
$appUrl = getenv('APP_URL');
if (strpos($appUrl, '/stage') !== false && strpos($url, '/stage') !== false) {
// If the app URL contains '/stage'
$secondSlashPosition = strpos($url, '/', strpos($url, '/') + 3);
$valueAfterSecondSlash = substr($url, $secondSlashPosition + 3);
} else {
// If the app URL does not contain '/stage'
$secondSlashPosition = strpos($url, '/', strpos($url, '/') + 1);
$valueAfterSecondSlash = substr($url, $secondSlashPosition + 1);
}
//url into substring
$parts = explode('-', $valueAfterSecondSlash);
$manufacturer = $parts[0];
$baseUrl = $this->router->generate('frontend.home.page', [], UrlGeneratorInterface::ABSOLUTE_URL);
// searching from category
if ($parts !== null) {
// Initialize an empty array to store LIKE conditions
$sqlCondition = array();
$parameters = array();
// Loop through $parts and build LIKE conditions
foreach ($parts as $index => $part) {
if (isset($part)) {
$placeholder = ":part$index";
$sqlCondition[] = 'name LIKE ' . $placeholder;
$parameters[$placeholder] = $part . '%';
}
}
// Combine the LIKE conditions using OR
$finalSQLClause = implode(' OR ', $sqlCondition);
// Build the final SQL statement with placeholders
$sql = <<<SQL
SELECT name
FROM category_translation
WHERE $finalSQLClause
LIMIT 1
SQL;
// Prepare the statement
$stmt = $this->connection->prepare($sql);
// Bind parameters
foreach ($parameters as $placeholder => $value) {
$stmt->bindValue($placeholder, $value);
}
// Execute the statement
$stmt->execute();
// Fetch the result
$aResults = $stmt->fetchColumn();
//dd($parts,$parameters,$sqlCondition,$finalSQLClause,$sql,$aResults);
if ($aResults) {
$slug = $this->slugify->slugify($aResults);
$manufacturerCategoryLink = $baseUrl . "$slug/";
if ($this->checkUrlStatus($manufacturerCategoryLink) == 200) {
$event->setResponse(new RedirectResponse($manufacturerCategoryLink));
return true;
}
}
}
//search from manufacture
$manufacturerCategoryLink = $baseUrl . "hersteller/$manufacturer/";
//if manufacturer is a single name
if ($this->checkUrlStatus($manufacturerCategoryLink) == 200) {
$event->setResponse(new RedirectResponse($manufacturerCategoryLink));
return true;
}
//if manufacturer has two words in his name e.g black-decker
$sql = <<<SQL
SELECT NAME
FROM product_manufacturer_translation
WHERE name LIKE :manufacturer OR name LIKE :part1
LIMIT 1
SQL;
// Prepare the statement
$stmt = $this->connection->prepare($sql);
// Bind parameters
$stmt->bindValue(':manufacturer', $manufacturer . '%');
$stmt->bindValue(':part1', $parts[1] . '%');
// Execute the statement
$stmt->execute();
// Fetch the result
$aResults = $stmt->fetchOne();
if ($aResults) {
$productManufacturerLink = $baseUrl . "hersteller/$manufacturer-$parts[1]/";
if ($this->checkUrlStatus($productManufacturerLink) == 200) {
$event->setResponse(new RedirectResponse($productManufacturerLink));
return true;
}
}
// Assuming $manufacturerCategoryLink is the variable containing the error
$error_message = "Error with manufacturer category link: " . $manufacturerCategoryLink;
// Logging the error message to a file
error_log($error_message, 3, "/var/tmp/my-errors.log");
return false;
// throw new ProductNotFoundException($productId);
$response = new RedirectResponse($request->server->get('APP_URL') . '/404');
$response->send();
exit;
}
}
}
private function getProductById(string $productId)
{
return $this->productRepository->search(
(new Criteria())->addFilter(new EqualsFilter('id', $productId)),
$this->context
)->first();
}
private function getCategoryRoute(string $productId): ?string
{
$product = $this->getProductById($productId);
if ($product) {
$categories = $product->getCategoryIds();
if (!empty($categories)) {
$sCatId = $categories[0];
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('foreignKey', $sCatId));
$seoPathInfo = $this->seoUrlRepository->search($criteria, Context::createDefaultContext())->first();
$sCatUrl = $seoPathInfo->getSeoPathInfo();
$fullUrl = rtrim(getenv('APP_URL'), '/') . '/' . ltrim($sCatUrl, '/');
return $fullUrl;
}
}
return null;
}
private function checkUrlStatus($url)
{
// Initialize cURL session
$ch = curl_init($url);
// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
// Execute cURL session
curl_exec($ch);
// Get HTTP status code
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Close cURL session
curl_close($ch);
return $statusCode;
}
public function onKernelException(ExceptionEvent $event)
{
$request = $event->getRequest();
$productId = $request->attributes->get('productId');
$exception = $event->getThrowable();
if ($exception instanceof ProductNotFoundException || $exception instanceof NotFoundHttpException) {
$product = null;
if ($productId != null) {
$product = $this->getProductById($productId);
}
//Product deactivaton block
if ($product) {
if (!$product->getActive()) {
$categoryRoute = $this->getCategoryRoute($productId);
if ($categoryRoute) {
$event->setResponse(new RedirectResponse($categoryRoute, 301));
return;
} else {
$homeLink = $this->router->generate('frontend.home.page', [], UrlGeneratorInterface::ABSOLUTE_URL);
$event->setResponse(new RedirectResponse($homeLink, 301));
return;
}
}
} else {
//Product deleted block
$url = $request->attributes->get('sw-original-request-uri');
if ($url) {
$appUrl = getenv('APP_URL');
if (strpos($appUrl, '/stage') !== false) {
// If the app URL contains '/stage'
$secondSlashPosition = strpos($url, '/', strpos($url, '/') + 3);
$valueAfterSecondSlash = substr($url, $secondSlashPosition + 3);
} else {
// If the app URL does not contain '/stage'
$secondSlashPosition = strpos($url, '/', strpos($url, '/') + 1);
$valueAfterSecondSlash = substr($url, $secondSlashPosition + 1);
}
//url into substring
$parts = explode('-', $valueAfterSecondSlash);
$manufacturer = $parts[0];
$baseUrl = $this->router->generate('frontend.home.page', [], UrlGeneratorInterface::ABSOLUTE_URL);
// searching from category
if ($parts !== null) {
// Initialize an empty array to store LIKE conditions
$sqlCondition = array();
$parameters = array();
// Loop through $parts and build LIKE conditions
foreach ($parts as $index => $part) {
if (isset($part)) {
$placeholder = ":part$index";
$sqlCondition[] = 'name LIKE ' . $placeholder;
$parameters[$placeholder] = $part . '%';
}
}
// Combine the LIKE conditions using OR
$finalSQLClause = implode(' OR ', $sqlCondition);
// Build the final SQL statement with placeholders
$sql = <<<SQL
SELECT name
FROM category_translation
WHERE $finalSQLClause
LIMIT 1
SQL;
// Prepare the statement
$stmt = $this->connection->prepare($sql);
// Bind parameters
foreach ($parameters as $placeholder => $value) {
$stmt->bindValue($placeholder, $value);
}
// Execute the statement
$stmt->execute();
$aResults = $stmt->fetchColumn();
if ($aResults) {
$slug = $this->slugify->slugify($aResults);
$manufacturerCategoryLink = $baseUrl . "$slug/";
if ($this->checkUrlStatus($manufacturerCategoryLink) == 200) {
$event->setResponse(new RedirectResponse($manufacturerCategoryLink));
return;
}
}
}
//search from manufacture
$manufacturerCategoryLink = $baseUrl . "marken/$manufacturer/";
//if manufacturer is a single name
if ($this->checkUrlStatus($manufacturerCategoryLink) == 200) {
$event->setResponse(new RedirectResponse($manufacturerCategoryLink));
return;
}
//if manufacturer has two words in his name e.g black-decker
$sql = <<<SQL
SELECT NAME
FROM product_manufacturer_translation
WHERE name LIKE :manufacturer OR name LIKE :part1
LIMIT 1
SQL;
// Prepare the statement
$stmt = $this->connection->prepare($sql);
// Bind parameters
$stmt->bindValue(':manufacturer', $manufacturer . '%');
$stmt->bindValue(':part1', $parts[1] . '%');
// Execute the statement
$stmt->execute();
// Fetch the result
$aResults = $stmt->fetchOne();
if ($aResults) {
$productManufacturerLink = $baseUrl . "marken/$manufacturer-$parts[1]/";
if ($this->checkUrlStatus($productManufacturerLink) == 200) {
$event->setResponse(new RedirectResponse($productManufacturerLink));
return;
}
}
$error_message = "Error with manufacturer category link: " . $manufacturerCategoryLink;
error_log($error_message, 3, "/var/tmp/my-errors.log");
return;
}
}
}
}
/**
* @param $str1
* @param $str2
* @return false|string
*/
public function longestConsecutiveSubstring($str1, $str2) {
$len1 = strlen($str1);
$len2 = strlen($str2);
$dp = [];
$maxLen = 0;
$endIndex = 0;
for ($i = 0; $i <= $len1; $i++) {
for ($j = 0; $j <= $len2; $j++) {
if ($i == 0 || $j == 0) {
$dp[$i][$j] = 0;
} elseif ($str1[$i - 1] == $str2[$j - 1]) {
$dp[$i][$j] = $dp[$i - 1][$j - 1] + 1;
if ($dp[$i][$j] > $maxLen) {
$maxLen = $dp[$i][$j];
$endIndex = $i - 1;
}
} else {
$dp[$i][$j] = 0;
}
}
}
if ($maxLen == 0) {
return '';
} else {
return substr($str1, $endIndex - $maxLen + 1, $maxLen);
}
}
/**
* @return array|\mixed[][]
* @throws \Doctrine\DBAL\Driver\Exception
* @throws \Doctrine\DBAL\Exception
*/
private function getAllFrontNavigationRoutes()
{
$sql = <<<SQL
SELECT
su.*
FROM
seo_url su,
`language` l
WHERE
su.route_name = 'frontend.navigation.page'
AND su.language_id = l.id
AND l.name = 'Deutsch'
SQL;
return $this->connection->executeQuery($sql)->fetchAllAssociative();
}
}