<?php
namespace Cms\CoreBundle\Service;
use Cms\CoreBundle\Model\Contexts\GlobalContext;
use Cms\CoreBundle\Model\Scenes\DashboardScenes\DocumentScene;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Cms\DomainBundle\Entity\Domain;
use Cms\TenantBundle\Entity\Tenant;
use Laminas\Uri\Uri;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* One of the most crucial pieces to the system is knowing what tenant is currently active, if any.
* This service is used to determine what tenant is active and sets up any needed hooks based on that.
* (One such thing that is set up is a global Doctrine SQL filter that always runs queries based on tenant information.)
*
* Class SystemSetup
* @package Cms\CoreBundle\Service
*/
final class SystemSetup implements EventSubscriberInterface
{
/**
* @var GlobalContext
*/
private GlobalContext $globalContext;
/**
* @var EntityManager
*/
private EntityManager $em;
/**
* @var string
*/
private string $stagingDomain;
/**
* @var SceneRenderer
*/
private SceneRenderer $sceneRenderer;
/**
* @var SceneRenderer
*/
private ParameterBagInterface $params;
/**
* @param ContextManager $contextManager
* @param EntityManager $em
* @param string $stagingDomain
* @param SceneRenderer $sceneRenderer
* @param ParameterBagInterface $params
*/
public function __construct(
ContextManager $contextManager,
EntityManager $em,
string $stagingDomain,
SceneRenderer $sceneRenderer,
ParameterBagInterface $params
) {
$this->globalContext = $contextManager->getGlobalContext();
$this->em = $em;
$this->stagingDomain = $stagingDomain;
$this->sceneRenderer = $sceneRenderer;
$this->params = $params;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['run', 9223372036854775806],
];
}
/**
* Listens to the "kernel.request" event to execute needed code to get things setup for CMS execution.
* THIS NEEDS TO RUN AT THE HIGHEST POSSIBLE PRIORITY IN THE LISTENER CHAIN!
*
* @param RequestEvent $event
* @throws \Exception
*/
public function run(RequestEvent $event): void
{
// make sure we only do this on the master request
if ( ! $event->isMainRequest()) {
return;
}
// make sure that the tenant is not set yet
if ( ! empty($this->globalContext->getTenant())) {
throw new \LogicException();
}
// not set, process the hostname
$host = $event->getRequest()->getHttpHost();
$dashboardHost = $this->globalContext->getDashboard(true);
// holder for tenant
$tenant = null;
$domain = null;
// try to match cases
if (preg_match('/^([-a-zA-Z0-9]+)\.([-a-zA-Z0-9]+)\.' . $this->stagingDomain . '$/', $host, $matches) === 1) {
// try and obtain the tenant
$tenant = $this->em->getRepository(Tenant::class)->findOneBySlug($matches[2]);
if ( ! $tenant) {
throw new NotFoundHttpException();
}
} else {
if (preg_match(
'/^([0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12})\\.' . preg_quote(
$dashboardHost,
'/',
) . '$/i',
$host,
$matches
) === 1) {
// obtain via uid
$tenant = $this->em->getRepository(Tenant::class)->findOneByUid($matches[1]);
if ( ! $tenant) {
throw new NotFoundHttpException();
}
} else {
if (preg_match(
'/^([-a-zA-Z0-9]+)\\.' . preg_quote($dashboardHost, '/') . '$/',
$host,
$matches
) === 1) {
// try and obtain the tenant
$tenant = $this->em->getRepository(Tenant::class)->findOneBySlug($matches[1]);
if ( ! $tenant) {
throw new NotFoundHttpException();
}
} else {
if ($host === $dashboardHost) {
// make sure tenant is null
$tenant = null;
} else {
if (preg_match(
'/^([-a-zA-Z0-9]+)\\.' . preg_quote('ngrok.io', '/') . '$/',
$host,
$matches
) === 1) {
// make sure tenant is null
$tenant = null;
} else {
// nothing handled specially, try to lookup domain
try {
$domain = $this->em->getRepository(Domain::class)->findOneByHost($host);
} catch (\Exception $e) {
$domain = null;
}
// TODO: need to eventually require all sites that need www redirect to be registered in the system...
// make sure we have one, if not either need to www redir or show error page
if ( ! $domain) {
// parse url
$url = new Uri($event->getRequest()->getUri());
// see if we are already www, if we are, show error page
if (str_starts_with(
$url->getHost(),
'www.'
)) {
$event->setResponse(
new Response(
$this->sceneRenderer->render(
new DocumentScene(
'@CmsCore/setup/noDomain.html.twig',
[
'host' => $host,
'debugging' => [
'kernel.environment' => $this->params->get('kernel.environment'),
'app.routing.apexes.system' => $this->params->get('app.routing.apexes.system'),
'app.routing.cluster' => $this->params->get('app.routing.cluster'),
'dashboard.hostname' => $this->params->get('dashboard.hostname'),
'_SERVER' => $_SERVER,
],
]
)
),
Response::HTTP_INTERNAL_SERVER_ERROR,
[]
)
);
$event->stopPropagation();
return;
}
// no www, do the www redirection
$url->setHost(
sprintf(
'www.%s',
$url->getHost()
)
);
$event->setResponse(
new RedirectResponse(
$url->toString(),
Response::HTTP_FOUND,
[]
)
);
$event->stopPropagation();
return;
}
// we do, set the tenant
$tenant = $domain->getTenant();
}
}
}
}
}
// set the tenant
$this->globalContext->escort(
function (GlobalContext $globalContext) use ($tenant) {
// set on the context
$globalContext->setTenant($tenant);
// set response header
/*
if ($tenant !== null) {
header(
sprintf(
'X-CAMPUSSUITE-TENANT: %s',
$tenant->getId()
),
true
);
}
*/
}
);
// TODO FIX THIS IN SSL STUFF!!!
// TODO: handle redirect domain (this needs cleanup)
/** @var Domain $domain */
if ($domain && $domain->getRedirection()->isEnabled()) {
if (strpos($event->getRequest()->getPathInfo(), '/.well-known/acme-challenge/') !== false) {
return;
}
$redirect = $domain->getRedirection();
$scheme = trim($event->getRequest()->getScheme());
// if marked as https upgrade, be sure to do that here
// technically this should be handled before doing the redirect, but we can shortcut an extra hop in the redirect chain doing it this way
if ($domain->isHttpsUpgrade()) {
$scheme = 'https';
}
$host = trim($redirect->getHost());
$path = '/' . ltrim(trim($redirect->getPath()), '/');
if ($redirect->getAppendPath() && ! empty(ltrim(trim($event->getRequest()->getPathInfo()), '/'))) {
$path = rtrim($path, '/') . '/' . ltrim(trim($event->getRequest()->getPathInfo()), '/');
}
$query = ( ! empty(trim($redirect->getAppendQuery()))) ? trim(
$event->getRequest()->getQueryString()
) : trim($redirect->getQuery());
$query = ltrim($query, '?');
if (empty($host)) {
throw new \RuntimeException();
}
if ( ! empty($path)) {
$path = '/' . ltrim($path, '/');
}
if ( ! empty($query)) {
$query = '?' . $query;
}
$url = $scheme . '://' . $host . $path . $query;
$event->setResponse(
new RedirectResponse(
$url,
$domain->getRealCode(),
[]
)
);
$event->stopPropagation();
}
// HACK: testing for root-level redirect here to www subdomain
// if the domain hostname is the same as the apex, we can assume root-level
// all we are basically going to do is use the same url, but prepend the www piece to the hostname
if ($domain && $domain->getApex()->getHost() === $domain->getHost()) {
// TODO: really need to figure out a better way to whitelist URLs that need to pass through...
if (strpos($event->getRequest()->getPathInfo(), '/.well-known/acme-challenge/') !== false) {
return;
}
$url = new Uri($event->getRequest()->getUri());
$url->setHost(
sprintf(
'www.%s',
$url->getHost()
)
);
$event->setResponse(
new RedirectResponse(
$url->toString(),
Response::HTTP_FOUND,
[]
)
);
$event->stopPropagation();
}
}
}