<?php
/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Enterprise License (PEL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PEL
*/
namespace Pimcore\Routing;
use Pimcore\Cache;
use Pimcore\Config;
use Pimcore\Http\Request\Resolver\SiteResolver;
use Pimcore\Http\RequestHelper;
use Pimcore\Model\Document;
use Pimcore\Model\Redirect;
use Pimcore\Model\Site;
use Pimcore\Model\Tool\Lock;
use Pimcore\Routing\Redirect\RedirectUrlPartResolver;
use Pimcore\Tool;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class RedirectHandler implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var RequestHelper
*/
private $requestHelper;
/**
* @var SiteResolver
*/
private $siteResolver;
/**
* @var Redirect[]
*/
private $redirects;
/**
* For BC, this is currently added as extra method call. The required annotation
* makes sure this is called via autowiring.
*
* TODO Pimcore 6 set as constructor dependency
*
* @required
*
* @param RequestHelper $requestHelper
*/
public function setRequestHelper(RequestHelper $requestHelper)
{
$this->requestHelper = $requestHelper;
}
/**
* For BC, this is currently added as extra method call. The required annotation
* makes sure this is called via autowiring.
*
* TODO Pimcore 6 set as constructor dependency
*
* @required
*
* @param SiteResolver $siteResolver
*/
public function setSiteResolver(SiteResolver $siteResolver)
{
$this->siteResolver = $siteResolver;
}
/**
* @param Request $request
* @param bool $override
*
* @return null|Response
*/
public function checkForRedirect(Request $request, $override = false)
{
// not for admin requests
if ($this->requestHelper->isFrontendRequestByAdmin($request)) {
return null;
}
// get current site if available
$sourceSite = null;
if ($this->siteResolver->isSiteRequest($request)) {
$sourceSite = $this->siteResolver->getSite($request);
}
$config = Config::getSystemConfig();
$partResolver = new RedirectUrlPartResolver($request);
foreach ($this->getFilteredRedirects($override) as $redirect) {
if (null !== $response = $this->matchRedirect($redirect, $request, $partResolver, $config, $sourceSite)) {
return $response;
}
}
}
private function matchRedirect(
Redirect $redirect,
Request $request,
RedirectUrlPartResolver $partResolver,
Config\Config $config,
Site $sourceSite = null
) {
if (empty($redirect->getType())) {
return null;
}
$matchPart = $partResolver->getRequestUriPart($redirect->getType());
$matches = [];
$doesMatch = false;
if ($redirect->isRegex()) {
$doesMatch = (bool)@preg_match($redirect->getSource(), $matchPart, $matches);
} else {
$source = str_replace('+', ' ', $redirect->getSource()); // see #2202
$doesMatch = $source === $matchPart;
}
if (!$doesMatch) {
return null;
}
// check for a site
if ($redirect->getSourceSite() || $sourceSite) {
if (!$sourceSite || $sourceSite->getId() !== $redirect->getSourceSite()) {
return null;
}
}
$target = $redirect->getTarget();
if (is_numeric($target)) {
$d = Document::getById($target);
if ($d instanceof Document\Page || $d instanceof Document\Link || $d instanceof Document\Hardlink) {
$target = $d->getFullPath();
} else {
$this->logger->error('Target of redirect {redirect} not found (Document-ID: {document})', [
'redirect' => $redirect->getId(),
'document' => $target
]);
return null;
}
}
$url = $target;
if ($redirect->isRegex()) {
array_shift($matches);
// support for pcre backreferences
$url = replace_pcre_backreferences($url, $matches);
}
if ($redirect->getTargetSite() && !preg_match('@http(s)?://@i', $url)) {
if ($targetSite = Site::getById($redirect->getTargetSite())) {
// if the target site is specified and and the target-path is starting at root (not absolute to site)
// the root-path will be replaced so that the page can be shown
$url = preg_replace('@^' . $targetSite->getRootPath() . '/@', '/', $url);
$url = $request->getScheme() . '://' . $targetSite->getMainDomain() . $url;
} else {
$this->logger->error('Site with ID {targetSite} not found', [
'redirect' => $redirect->getId(),
'targetSite' => $redirect->getTargetSite()
]);
return null;
}
} elseif (!preg_match('@http(s)?://@i', $url) && $config->general->domain) {
// prepend the host and scheme to avoid infinite loops when using "domain" redirects
$url = $request->getScheme() . '://' . $config->general->domain . $url;
}
// pass-through parameters if specified
$queryString = $request->getQueryString();
if ($redirect->getPassThroughParameters() && !empty($queryString)) {
$glue = '?';
if (strpos($url, '?')) {
$glue = '&';
}
$url .= $glue;
$url .= $queryString;
}
$statusCode = $redirect->getStatusCode() ?: Response::HTTP_MOVED_PERMANENTLY;
$response = new RedirectResponse($url, $statusCode);
$response->headers->set('X-Pimcore-ID', $redirect->getId());
// log all redirects to the redirect log
\Pimcore\Log\Simple::log(
'redirect',
Tool::getAnonymizedClientIp() . " \t Custom-Redirect ID: " . $redirect->getId() . ', Source: ' . $_SERVER['REQUEST_URI'] . ' -> ' . $url
);
return $response;
}
/**
* @return Redirect[]
*/
private function getRedirects()
{
if (null !== $this->redirects && is_array($this->redirects)) {
return $this->redirects;
}
$cacheKey = 'system_route_redirect';
if (($this->redirects = Cache::load($cacheKey)) === false) {
// acquire lock to avoid concurrent redirect cache warm-up
Lock::acquire($cacheKey);
//check again if redirects are cached to avoid re-warming cache
if (($this->redirects = Cache::load($cacheKey)) === false) {
try {
$list = new Redirect\Listing();
$list->setCondition('active = 1');
$list->setOrder('DESC');
$list->setOrderKey('priority');
$this->redirects = $list->load();
Cache::save($this->redirects, $cacheKey, ['system', 'redirect', 'route'], null, 998, true);
} catch (\Exception $e) {
$this->logger->error('Failed to load redirects');
}
}
Lock::release($cacheKey);
}
if (!is_array($this->redirects)) {
$this->logger->warning('Failed to load redirects', [
'redirects' => $this->redirects
]);
$this->redirects = [];
}
return $this->redirects;
}
/**
* @param bool $override
*
* @return Redirect[]
*/
private function getFilteredRedirects($override = false)
{
$now = time();
return array_filter($this->getRedirects(), function (Redirect $redirect) use ($override, $now) {
// this is the case when maintenance did't deactivate the redirect yet but it is already expired
if (!empty($redirect->getExpiry()) && $redirect->getExpiry() < $now) {
return false;
}
if ($override) {
// if override is true the priority has to be 99 which means that overriding is ok
return (int)$redirect->getPriority() === 99;
} else {
return (int)$redirect->getPriority() !== 99;
}
});
}
}