<?php
namespace Cms\FrontendBundle\Service;
use App\Service\MediaDecorator;
use App\Subscriber\HstsSubscriber;
use Cms\AssetsBundle\Model\Structure\MetaStructure;
use Cms\AssetsBundle\Model\Structure\TitleStructure;
use Cms\ContainerBundle\Doctrine\ContainerRepository;
use Cms\ContainerBundle\Doctrine\Containers\IntranetContainerRepository;
use Cms\ContainerBundle\Entity\Container;
use Cms\ContainerBundle\Entity\Containers\GenericContainer;
use Cms\ContainerBundle\Entity\Containers\IntranetContainer;
use Cms\ContainerBundle\Entity\Containers\PersonalContainer;
use Cms\ContainerBundle\Service\ContainerService;
use Cms\ContentBundle\Service\FrontendRenderer;
use Cms\CoreBundle\Model\Interfaces\Identifiable\IdentifiableInterface;
use Cms\CoreBundle\Service\Aws\S3Wrapper;
use Cms\CoreBundle\Service\ContextManager;
use Cms\CoreBundle\Service\SceneRenderer;
use Cms\CoreBundle\Service\Slugger;
use Cms\CoreBundle\Service\Transcoding\Transcoder;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Cms\DomainBundle\Doctrine\DomainRepository;
use Cms\DomainBundle\Entity\Domain;
use Cms\DomainBundle\Entity\SslCertificate;
use Cms\FileBundle\Entity\Nodes\File;
use Cms\FileBundle\Entity\Nodes\Files\ImageFile;
use Cms\FileBundle\Entity\Nodes\Folder;
use Cms\FileBundle\Entity\Optimizations\ImageOptimization;
use Cms\FileBundle\Service\MimeHelper;
use Cms\FrontendBundle\Exception\FrontendBuilderException;
use Cms\FrontendBundle\Model\FrontendGlobals;
use Cms\ModuleBundle\Doctrine\ModuleSettingsRepository;
use Cms\ModuleBundle\Entity\Draft;
use Cms\ModuleBundle\Entity\History;
use Cms\ModuleBundle\Entity\ModuleEntity;
use Cms\ModuleBundle\Entity\ModuleSettings;
use Cms\ModuleBundle\Entity\Proxy;
use Cms\ModuleBundle\Entity\Revision;
use Cms\ModuleBundle\Model\ModuleConfig;
use Cms\ModuleBundle\Model\Traits\FrontendActions\SocialMetaTrait;
use Cms\ModuleBundle\Service\PublicationService;
use Cms\ModuleBundle\Service\ModuleManager;
use Cms\ModuleBundle\Util\ModuleProxyPlaceholderDoctrineFilter;
use Cms\Modules\PageBundle\Service\PageModuleConfig;
use Cms\RedirectBundle\Doctrine\RedirectRepository;
use Cms\RedirectBundle\Entity\Redirect;
use Cms\TenantBundle\Entity\Tenant;
use Cms\TenantBundle\Model\ProductsBitwise;
use Cms\ThemeBundle\Entity\InnerLayout;
use Cms\ThemeBundle\Entity\OuterLayout;
use Cms\ThemeBundle\Entity\Template;
use Cms\ThemeBundle\Model\Package;
use Cms\ThemeBundle\Service\PackageManager;
use Cms\ThemeBundle\Service\ThemeManager;
use Cms\ThemeBundle\Service\Twig\Loader\DatabaseLoader;
use Cms\WidgetBundle\Model\WidgetObject;
use Cms\Widgets\Html\Html;
use Common\Util\Requests;
use Platform\SecurityBundle\Doctrine\Identity\AccountRepository;
use Products\NotificationsBundle\Entity\Notifications\Message;
use Products\SocialBundle\Entity\SocialAccount;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Platform\SecurityBundle\Entity\Identity\Account;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Twig\Environment;
/**
* Class FrontendBuilder
* @package Cms\FrontendBundle\Service
*/
final class FrontendBuilder
{
const ACCOUNT_UID_KEY = 'campussuiteAccountUid';
const QUERIES_SKIP = array(
// Google Analytics query string tracking variables
'utm_source',
'utm_medium',
'utm_term',
'utm_content',
'utm_campaign',
);
/**
* @var EntityManager
*/
private EntityManager $em;
/**
* @param ParameterBagInterface $params
* @param ContainerService $containerService
* @param DatabaseLoader $databaseLoader
* @param EntityManager $em
* @param Environment $twig
* @param FrontendRenderer $frontendRenderer
* @param ModuleManager $moduleManager
* @param PackageManager $packageManager
* @param S3Wrapper $s3Wrapper
* @param SceneRenderer $sceneRenderer
* @param ThemeManager $themeManager
* @param Transcoder $transcoder
* @param ContextManager $contextManager
* @param PublicationService $publicationService
* @param MimeHelper $mimeHelper
* @param MediaDecorator $mediaDecorator
*/
public function __construct(
ParameterBagInterface $params,
ContainerService $containerService,
DatabaseLoader $databaseLoader,
EntityManager $em,
Environment $twig,
FrontendRenderer $frontendRenderer,
ModuleManager $moduleManager,
PackageManager $packageManager,
S3Wrapper $s3Wrapper,
SceneRenderer $sceneRenderer,
ThemeManager $themeManager,
Transcoder $transcoder,
ContextManager $contextManager,
PublicationService $publicationService,
MimeHelper $mimeHelper,
MediaDecorator $mediaDecorator
) {
$this->params = $params;
$this->containerService = $containerService;
$this->databaseLoader = $databaseLoader;
$this->em = $em;
$this->moduleManager = $moduleManager;
$this->packageManager = $packageManager;
$this->frontendRenderer = $frontendRenderer;
$this->s3 = $s3Wrapper;
$this->sceneRenderer = $sceneRenderer;
$this->themeManager= $themeManager;
$this->transcoder = $transcoder;
$this->twig = $twig;
$this->contextManager = $contextManager;
$this->publicationService = $publicationService;
$this->mimeHelper = $mimeHelper;
$this->mediaDecorator = $mediaDecorator;
}
/**
* @return DomainRepository
*/
private function domainRepository()
{
return $this->em->getRepository(Domain::class);
}
/**
* @return RedirectRepository
*/
private function redirectRepository()
{
return $this->em->getRepository(Redirect::class);
}
/**
* @return AccountRepository
*/
private function accountRepository()
{
return $this->em->getRepository(Account::class);
}
/**
* @return ContainerRepository
*/
private function containerRepository()
{
return $this->em->getRepository(Container::class);
}
/**
* @return IntranetContainerRepository
*/
private function intranetContainerRepository()
{
return $this->em->getRepository(IntranetContainer::class);
}
/**
* @return string
*/
private function determineEnvironment(): string
{
return $this->params->get('kernel.environment');
}
/**
* @return string
*/
private function determineDashboard(): string
{
return $this->params->get('dashboard.hostname');
}
/**
* @return Tenant
*/
private function determineTenant(): Tenant
{
$tenant = $this->contextManager->getGlobalContext()->getTenant();
if ( ! $tenant) {
throw new \RuntimeException();
}
return $tenant;
}
/**
* @param ModuleEntity $entity
* @return Response
* @throws \Exception
*/
public function preview(ModuleEntity $entity)
{
// determine the proxy
$proxy = null;
switch (true) {
case $entity instanceof Proxy:
$proxy = $entity;
break;
case $entity instanceof History:
$proxy = $entity->getProxy();
break;
case $entity instanceof Draft:
$proxy = $entity->getProxy();
break;
case $entity instanceof Revision:
$proxy = $entity->getDraft()->getProxy();
break;
default:
throw new \Exception();
}
// generate the url for the module entity
$url = $this->publicationService->getFrontLink($proxy, false);
// generate the preview
$preview = $this->build(
Request::create($url),
function (FrontendGlobals $globals) use ($entity) {
// set the override content to like a draft, history, or revision
$globals->setThingOverride($entity);
// ensure no mimic
$globals->setMimic(null);
// need to set the base
$globals->getAssetsOrganizer()->getBaseTag()->set(
'href',
$globals->pathPrefix()
);
// due to how the lookup is needed to be made, need to disable our filter for placeholders
$this->em->getFilters()
->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
->setParameter(
'mode',
ModuleProxyPlaceholderDoctrineFilter::MODES__ANY
);
}
);
return $preview;
}
/**
* @param mixed|Request|FrontendGlobals $input
* @return FrontendGlobals
* @throws \Exception
*/
public function globals($input)
{
// create the globals if not given
switch (true) {
case is_string($input):
$globals = new FrontendGlobals(Requests::factory($input));
break;
case $input instanceof FrontendGlobals:
$globals = $input;
break;
case $input instanceof Request:
$globals = new FrontendGlobals($input);
break;
default:
throw new \Exception();
}
// determine if we should do debugging
$debugging = $this->contextManager->getGlobalContext()->getTenant();
$globals->debugging( ! empty($debugging) && $debugging->getDebugging() === true);
// save the environment
$globals->debugStart('setEnvironment');
$globals->setEnvironment(
$this->determineEnvironment()
);
$globals->debugStop('setEnvironment');
// obtain the tenant
$globals->debugStart('setTenant');
$globals->setTenant(
$this->determineTenant()
);
$globals->debugStop('setTenant');
// save the dashboard hostname
$globals->debugStart('setDashboard');
$globals->setDashboard(
$this->determineDashboard()
);
$globals->debugStop('setDashboard');
// obtain just the url
$globals->debugStart('setPath');
$globals->setPath(
$this->determinePath($globals)
);
$globals->debugStop('setPath');
// get the domain entity tied to this site
$globals->debugStart('setDomain');
$globals->setDomain(
$this->determineDomain($globals)
);
$globals->debugStop('setDomain');
// determine if we are running a personal site
$globals->debugStart('setAccount');
$globals->setAccount(
$this->determineAccount($globals)
);
$globals->debugStop('setAccount');
// deterime the website for this (i.e. root container)
$globals->debugStart('setSite');
$globals->setSite(
$this->determineSite($globals)
);
$globals->debugStop('setSite');
// get the path of containers to where we are at
$globals->debugStart('setContainers');
$globals->setContainers(
$this->determineContainers($globals)
);
$globals->debugStop('setContainers');
// set the specific container
$globals->debugStart('setContainer');
$globals->setContainer(
$this->determineContainer($globals)
);
$globals->debugStop('setContainer');
// get mimicking account if any
$globals->debugStart('setMimic');
$globals->setMimic(
$this->determineMimic($globals)
);
$globals->debugStop('setMimic');
// get the theme for the container we are at
$globals->debugStart('setTheme');
$globals->setTheme(
$this->determineTheme($globals)
);
$globals->debugStop('setTheme');
// get the package for the theme
$globals->debugStart('setPackage');
$globals->setPackage(
$this->determinePackage($globals)
);
$globals->debugStop('setPackage');
// go ahead and get an outer layout
$globals->debugStart('setOuterLayout');
$globals->setOuterLayout(
$this->determineOuterLayout($globals)
);
$globals->debugStop('setOuterLayout');
// go ahead and get an inner layout, this may be changed later by module specific code
$globals->debugStart('setInnerLayout');
$globals->setInnerLayout(
$this->determineInnerLayout($globals)
);
$globals->debugStop('setInnerLayout');
// build out the generated navigation
$globals->debugStart('setNavigations');
$globals->setNavigations(
$this->determineNavigations($globals)
);
$globals->debugStop('setNavigations');
// get the module type
$globals->debugStart('setModule');
$globals->setModule(
$this->determineModule($globals)
);
$globals->debugStop('setModule');
// figure out what piece of the path is left; should either be nothing or and id/slug combo
$globals->debugStart('setExtra');
$globals->setExtra(
$this->determineExtra($globals)
);
$globals->debugStop('setExtra');
// set the module settings
$globals->debugStart('setModuleSettings');
$globals->setModuleSettings(
$this->determineModuleSettings($globals)
);
$globals->debugStop('setModuleSettings');
// HACK: social support
$globals->debugStart('setSocialSupport');
$globals->setSocialSupport(
$this->determineSocialSupport($globals)
);
$globals->debugStop('setSocialSupport');
// done
return $globals;
}
/**
* @param Request $request
* @return bool
*/
private function checkMimic(Request $request)
{
return $request->query->has('_mimic');
}
/**
* @param FrontendGlobals $globals
* @return bool
*/
private function determineSocialSupport(FrontendGlobals $globals)
{
if ($globals->getTenant()->getProducts()->checkFlag(ProductsBitwise::SMM__BASE)) {
return ( ! empty($this->em->getRepository(SocialAccount::class)->findOneBy(array())));
}
return false;
}
/**
* @param FrontendGlobals $globals
* @return Container
*/
private function determineContainer(FrontendGlobals $globals)
{
if (count($globals->getContainers()) > 0) {
return $globals->getContainers()[0];
}
return $globals->getSite();
}
/**
* @return bool
*/
private function preventRedirect()
{
return ($this->contextManager->getGlobalContext()->getTenant()->getRedirects() !== true);
}
/**
* @param Request $request
* @return Redirect
*/
private function checkRedirection(Request $request)
{
if ($this->preventRedirect()) {
return null;
}
// check for testing override and set host properly
$host = null;
if ($request->headers->has('X-CAMPUSSUITE-REDIRECTS-CHECKER-HOSTNAME')) {
$host = $request->headers->get('X-CAMPUSSUITE-REDIRECTS-CHECKER-HOSTNAME');
}
if (empty($host)) {
$host = $request->getHost();
}
// get the path and break it up
$path = $request->getPathInfo();
$queries = $request->query->all();
// filter queries
if (count($queries)) {
$queries = array_filter(
$queries,
function ($key) {
return ( ! in_array($key, self::QUERIES_SKIP));
},
ARRAY_FILTER_USE_KEY
);
}
// add queries to path
if (count($queries)) {
$path = sprintf('%s?%s', $path, http_build_query($queries));
}
// attempt to grab the domain
$domain = $this->domainRepository()->findOneByHost($host);
// if no domain, no redirects to check
if (empty($domain)) {
return null;
}
// check static redirect first
// TODO: need to support query string in source urls; so we can support redirecting things like "/page.asp?id=1234" which is a must
$redirect = $this->redirectRepository()->findActiveStaticRedirect($domain, $path);
if ( ! empty($redirect)) {
return $redirect;
}
// check against wildcard redirects
$redirect = $this->redirectRepository()->findActiveWildcardRedirect($domain, $path);
if ( ! empty($redirect)) {
return $redirect;
}
// check for uppercase symbols in path
// if there are any, redirect to the same path, but in lower case
// this should only check the path portion of the url
// upper case should be allowed in query string
// also, need to ensure that google analytics is included in the query string still too
// with that, this code needs to use the original request information
// we also return own own redirect entity type to make code easier, as this method should return that type or null otherwise
// TODO: does this affect query params when their handling becomes more complex?
if (preg_match('/[A-Z]/', $request->getPathInfo())) {
$fixed = strtolower($request->getPathInfo());
if ($request->query->count() > 0) {
$fixed .= '?' . http_build_query($request->query->all());
}
return (new Redirect())
->setDomain($domain)
->setDestination($fixed)
->setCode(Redirect::CODES__302)
->setStatus(Redirect::STATUSES__ACTIVE);
}
// no matches
return null;
}
/**
* @param Request $request
* @param Redirect $redirect
* @return RedirectResponse
* @throws \Exception
*/
private function handleRedirection(Request $request, Redirect $redirect)
{
if ($this->preventRedirect()) {
return null;
}
$dest = $redirect->getDestination();
$src = $redirect->getSource();
// prevent circular redirects
if ($dest === $src) {
return null;
}
// detect wildcard redirects
$wildcardRegexp = '/(.*)\\/\\*$/';
if (preg_match($wildcardRegexp, $dest, $destMatches) === 1 && preg_match($wildcardRegexp, $src, $srcMatches) === 1) {
// dest and source should both end in /* now
// remove the common part from the url
if ($srcMatches[1] !== '' && mb_strpos(mb_strtolower($request->getPathInfo()), mb_strtolower($srcMatches[1])) !== 0) {
return null;
}
// get what we need to attach to the end of the destination
$srcChunk = mb_substr($request->getPathInfo(), mb_strlen($srcMatches[1]));
// do the replacement
$dest = preg_replace('/\\/\\*$/', ('/' . ltrim($srcChunk, '/')), $dest);
}
// if no destination or it has a wrong wildcard pattern, we can't do anything
if (empty($dest) || preg_match('/\\/\\*$/', $dest) === 1) {
return null;
}
// fix the destination
if (preg_match('/^https?:\\/\\//', $dest) !== 1) {
// ensure front slash
if (substr($dest, 0, 1) !== '/') {
$dest = '/' . $dest;
}
// attach the original domain to it
$dest = $request->getScheme() . '://' . $request->getHost() . $dest;
}
// generate the base request for the redirection
// handle fragments; they get lost when parsing as a request
$parts = explode('#', $dest);
$dest = array_shift($parts);
$fragment = ( ! empty($parts)) ? implode('#', $parts) : null;
$base = Request::create($dest);
$queries = array_filter(
$request->query->all(),
function ($key) {
return in_array($key, self::QUERIES_SKIP);
},
ARRAY_FILTER_USE_KEY
);
$queries = array_merge($queries, $base->query->all());
// attach query info from original request to new one
$base = $base->duplicate(null, null, null, null, null, array_merge(
$base->server->all(),
array(
// this will set the query string, so need to append the two together first
// TODO: do we need setting to toggle adding incoming query string or not?
// TODO: need to change the order these get appended; some systems require them in order
'QUERY_STRING' => http_build_query($queries),
)
));
// get final string, and add fragment if relevant
$base = $base->getUri();
if ( ! empty($fragment)) {
$base .= '#' . $fragment;
}
// generate the actual redirect response
return new RedirectResponse(
$base,
$redirect->getRealCode(),
array(
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'redirect:%s',
$redirect->getId()
),
)
);
}
/**
* @param Request $request
* @param Domain $domain
*
* @return RedirectResponse|null
*/
private function checkDomainBehaviorRedirect(Request $request, Domain $domain)
{
if (empty($domain->getId())) {
if ( ! $site = $this->resolveSiteByStagingDomain($domain)) {
return null;
}
if ( ! $site->getDomain()) {
return null;
}
if ($request->getHost() === $site->getDomain()->getHost()) {
return null;
}
return new RedirectResponse(
$request->getScheme() . '://' . $site->getDomain()->getHost() . $request->getRequestUri(),
RedirectResponse::HTTP_FOUND,
[
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'domain:%s',
$domain->getId()
),
]
);
}
// if not secure coming in, but needs to be
if ($domain instanceof Domain && $domain->getCertificate() instanceof SslCertificate && $domain->isHttpsUpgrade() && ! $request->isSecure()) {
return new RedirectResponse(
'https://' . $request->getHost() . $request->getRequestUri(),
RedirectResponse::HTTP_FOUND,
array(
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'ssl:%s',
$domain->getCertificate()->getId()
),
)
);
}
// by default, no redirection
return null;
}
/**
* @param Request $request
* @param FrontendGlobals $globals
*
* @return RedirectResponse|null
*/
private function checkPersonalSiteSlugRedirect(Request $request, FrontendGlobals $globals)
{
// check if we are a personal site
if ($globals->getSite() instanceof PersonalContainer) {
// generate the slug that we should be
$expected = '~'.strtolower($globals->getAccount()->getEmail());
// obtain the slug from the front url pattern
$actual = explode('/', trim(strtolower($globals->getPath()), '/'))[0];
if ($actual !== $expected) {
return new RedirectResponse(
rtrim(sprintf(
'%s://%s/%s?%s',
$request->getScheme(),
$request->getHost(),
str_replace($actual, $expected, trim(strtolower($request->getPathInfo()), '/')),
$request->getQueryString()
), '?'),
RedirectResponse::HTTP_FOUND,
array(
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'personal:%s',
$globals->getAccount()->getUid()
),
)
);
}
}
// by default, no redirection
return null;
}
/**
* @param FrontendGlobals $globals
* @param array|WidgetObject[] $widgets
* @return string
*/
public function renderCustom(FrontendGlobals $globals, array $widgets): string
{
return $this->render($globals, [
'content' => $widgets,
]);
}
/**
* @param Request $request
* @param callable|null $callback
* @throws FrontendBuilderException
* @throws \Exception
* @return Response
*/
public function build(Request $request, callable $callback = null)
{
return $this->secureWithHSTS(
$request,
$this->buildWithoutHSTS($request, $callback)
);
}
/**
* @param Request $request
* @param callable|null $callback
* @throws FrontendBuilderException
* @throws \Exception
* @return Response
*/
private function buildWithoutHSTS(Request $request, callable $callback = null): Response
{
// see if we need to trigger mimic functionality and do that if so
if ($this->checkMimic($request)) {
return $this->handleMimic($request);
}
// see if we need to do a redirect
if ( ! empty($redirect = $this->checkRedirection($request))) {
$response = $this->handleRedirection($request, $redirect);
if ( ! empty($response)) {
return $response;
}
}
// enable the filter for keeping out placeholder proxies
$this->em->getFilters()->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
->setParameter('mode', ModuleProxyPlaceholderDoctrineFilter::MODES__NO_PLACEHOLDERS);
$this->em->getFilters()->enable(ModuleProxyPlaceholderDoctrineFilter::FILTER);
// obtain globals
$globals = $this->globals($request);
// run callback if one
if (is_callable($callback)) {
call_user_func($callback, $globals);
}
// ensure that request schema matches the domains behavior setting if it is set
$globals->debugStart('checkDomainBehaviorRedirect');
$response = $this->checkDomainBehaviorRedirect($request, $globals->getDomain());
if ( ! empty($response)) {
return $response;
}
$globals->debugStop('checkDomainBehaviorRedirect');
// if doing a teacher site, make sure the incoming url matches the proper email address
$globals->debugStart('checkPersonalSiteSlugRedirect');
$response = $this->checkPersonalSiteSlugRedirect($request, $globals);
if ( ! empty($response)) {
return $response;
}
$globals->debugStop('checkPersonalSiteSlugRedirect');
// run the controller for the module to get a response
$response = null;
$globals->debugStart('runController');
$response = $this->runController($globals);
$globals->debugStop('runController');
// TODO: is this needed anymore?
$response->headers->setCookie(
new Cookie(
'campussuite.tenant.id',
$globals->getTenant()->getId(),
0,
'/',
null,
false,
false
)
);
// done
return $response;
}
/**
* @param Request $request
* @param Response $response
* @return Response
* @throws \Exception
*/
private function secureWithHSTS(Request $request, Response $response): Response
{
$globals = $this->globals($request);
$domain = $globals->getDomain();
if ( ! $domain instanceof Domain) {
return $response;
}
$certificate = $domain->getCertificate();
if ( ! $domain->getCertificate() instanceof SslCertificate) {
return $response;
}
$certificateAge = (time() - $certificate->getCreatedAt()->getTimestamp());
$isCertificateMoreThanDayOld = ((24 * 60 * 60) < $certificateAge);
if ( ! $isCertificateMoreThanDayOld) {
return $response;
}
$maxAge = HstsSubscriber::HSTS_MAX_AGE;
$response->headers->add(['Strict-Transport-Security' => "max-age=$maxAge"]);
return $response;
}
/**
* @param FrontendGlobals $globals
* @return string
*/
private function determinePath(FrontendGlobals $globals)
{
return rawurldecode($globals->getRequest()->getPathInfo());
}
/**
* @param FrontendGlobals $globals
* @return Account
* @throws \Exception
*/
private function determineMimic(FrontendGlobals $globals)
{
// handling of this depends on the type of container/site we are in
switch (true) {
case $globals->getSite() instanceof IntranetContainer:
// we are still logged in and on the dashboard, pull from the context
return $this->contextManager->getGlobalContext()->getEffectiveAccount();
default:
// obtain the uid of the mimic user from cookies
$uid = $globals->getRequest()->cookies->get(self::ACCOUNT_UID_KEY);
// if not null, find the account
if ( ! empty($uid)) {
return $this->accountRepository()->findExactByUid($uid);
}
}
// no mimic
return null;
}
/**
* @param FrontendGlobals $globals
* @return Account
* @throws \Exception
*/
private function determineAccount(FrontendGlobals $globals)
{
// get the slugs
$pieces = $this->explodePath($globals->getPath());
// see if we match the regex, if not, we are not running a personal site
if (preg_match('/^[~](.+)$/', $pieces[0], $matches) !== 1
|| ($globals->getDomain()->getHost()
=== $this->contextManager->getGlobalContext()->getDashboard())
) {
return null;
}
// what is left should be a username
// need to perform a lookup
$account = $this->accountRepository()->findByEmailWithPartialFallback($matches[1]);
// if we get nothing, then that means somebody is requesting a teacher site that does not exist
if (count($account) !== 1 || ! $account['0'] instanceof Account) {
throw new BadRequestHttpException();
}
// done
return $account['0'];
}
/**
* @param FrontendGlobals $globals
* @return Response
* @throws \Exception
*/
private function runController(FrontendGlobals $globals)
{
// get the module config
$module = $globals->getModule();
$container = $globals->getContainer();
// try to run the controller
try {
// if module is inactive, may need to throw a not found exception
$active = $this->containerService->isModuleActive($container, $module->name());
// do the processing
$globals->debugStart('runControllerFrontend');
$result = $module->frontend($globals);
$globals->debugStop('runControllerFrontend');
// HACK: for special widget cases
// @see https://campussuiteteam.slack.com/archives/C0ZRTD8MS/p1533838566000422
// if the container on an item is not the same, check it for module enabling
$thing = $globals->getThing();
if ( ! $active && $thing instanceof Proxy && $thing->getContainer() !== $container) {
$active = $this->containerService->isModuleActive($thing->getContainer(), $module->name());
}
if ( ! $active) {
throw new NotFoundHttpException();
}
// common metadata about content
try {
if ($globals->getTenant() instanceof Tenant) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:tenant',
$globals->getTenant()->getId()
));
}
if ($globals->getSite() instanceof Container) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:site',
$globals->getSite()->getId()
));
}
if ($globals->getContainer() instanceof Container) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:department',
$globals->getContainer()->getId()
));
}
if ($globals->getTheme() instanceof Template) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:theme',
$globals->getTheme()->getId()
));
}
if ($globals->getOuterLayout() instanceof OuterLayout) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:outer_layout',
$globals->getOuterLayout()->getId()
));
}
if ($globals->getInnerLayout() instanceof InnerLayout) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:inner_layout',
$globals->getInnerLayout()->getId()
));
}
if ($globals->getModule() instanceof ModuleConfig) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:module',
$globals->getModule()->key()
));
}
if ($globals->getThing() instanceof IdentifiableInterface) {
$globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
'campussuite:content',
$globals->getThing()->getId()
));
}
} catch (\Exception $e) {
// noop
}
// if we were returned an exception, throw it
if ($result instanceof \Exception) {
throw $result;
}
// if we were given a response directly, return it
if ($result instanceof Response) {
return $result;
}
// if we don't get the expected return back, throw error
if ( ! is_array($result)) {
throw new \Exception();
}
// default stuff just to be safe
$result = $result + array(
[],
new Response()
);
} catch (\Exception $e) {
// attempt to handle the exception
return $this->handleControllerException($globals, $e);
}
// get the pieces from the result
/** @var array $content */
/** @var Response $response */
[$content, $response] = $result;
// now that the main logic has been completed, we can ensure that non-live data is not returned again
$this->em->getFilters()->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
->setParameter('mode', ModuleProxyPlaceholderDoctrineFilter::MODES__NO_PLACEHOLDERS);
$this->em->getFilters()->enable(ModuleProxyPlaceholderDoctrineFilter::FILTER);
// render and return
$globals->debugStart('runControllerRender');
$rendered = $this->render($globals, $content);
$globals->debugStop('runControllerRender');
// fix the content on the response and set proper headers
$response
->setContent($rendered)
->setStatusCode(200)
->headers->set('Content-Type', 'text/html')
;
// done
return $response;
}
/**
* @param FrontendGlobals $globals
* @param \Exception $e
* @return Response
* @throws \Exception
*/
private function handleControllerException(FrontendGlobals $globals, \Exception $e)
{
// determine the code
switch (true) {
case $e instanceof NotFoundHttpException:
$globals->getAssetsOrganizer()->getTitles()->add(
new TitleStructure('404 Not Found')
);
$code = 404;
break;
case $e instanceof AccessDeniedHttpException:
$globals->getAssetsOrganizer()->getTitles()->add(
new TitleStructure('403 Forbidden')
);
$code = 403;
break;
case $e instanceof BadRequestHttpException:
return new Response(null, 400, []);
default:
$globals->getAssetsOrganizer()->getTitles()->add(
new TitleStructure('500 Server Error')
);
$code = 500;
}
// render the page content
$rendered = $this->render($globals, array(
'content' => array(
(new Html())
->setContent($this->twig->render(
$this->databaseLoader->determine(
$globals->getTheme(),
sprintf(
'/system/http/%s/build/tpl.html.twig',
$code
)
),
array(
'code' => $code,
'error' => $e,
'_globals' => $globals->asArray(),
)
))
),
));
// send back a response
return (new Response(
$rendered,
$code,
array(
'Content-Type' => 'text/html'
)
));
}
/**
* @param FrontendGlobals $globals
* @param mixed $content
* @return string
*/
private function render(FrontendGlobals $globals, $content): string
{
if ( ! is_array($content)) {
$content = [];
}
$this->frontendRenderer->prepare($globals, FrontendRenderer::MODES__LIVE);
return $this->frontendRenderer->renderPage($content);
}
/**
* @param FrontendGlobals $globals
* @return Domain
* @throws \Exception
*/
private function determineDomain(FrontendGlobals $globals)
{
// obtain the hostname
$host = $globals->getRequest()->getHost();
// determine the domain for this hostname
$domain = $this->domainRepository()->findOneByHost($host);
// throw error if we found one that was not configured
if ($domain === null) {
return (new Domain())
->setName($host);
}
// now we need the apex
$apex = $domain->getApex();
// if the apex is not verified, we have a problem
/*
if ( ! $apex->getVerification()->isVerified()) {
throw FrontendBuilderException::apexNotVerified($apex);
}
*/
// all is good, send back the domain, the apex is already loaded and linked to it
return $domain;
}
/**
* @param FrontendGlobals $globals
* @return Container
* @throws \Exception
*/
private function determineSite(FrontendGlobals $globals)
{
// obtain the domain
$domain = $globals->getDomain();
// init the site
$site = null;
// handle based on the domain type
switch (true) {
case ! empty($domain->getId()):
// get basic container
$site = $this->em->getRepository(GenericContainer::class)->findOneByDomain($domain);
if ( ! $site) {
throw new NotFoundHttpException(sprintf(
'No site is linked to the domain "%s".',
$domain->getHost()
));
}
break;
case empty($domain->getId()):
$site = $this->resolveSiteByStagingDomain($domain);
// handle intranets
if ($domain->getHost() === $this->contextManager->getGlobalContext()->getDashboard()) {
// in this case, the next chunk of the slug should be a slug of an intranet container
$pieces = $this->explodePath($globals->getPath());
// if we don't have at least one pieces, we have a problem
if (count($pieces) < 1) {
throw new BadRequestHttpException();
}
// lookup based on this info
$site = $this->intranetContainerRepository()->findOneBy(array(
'parent' => null,
'slug' => ltrim($pieces[0], '~'),
));
}
break;
default:
throw FrontendBuilderException::domainUnhandled($domain);
}
// if we are doing a personal site, need to possibly tweak this
if ($globals->getAccount() !== null) {
// in this case, the next chunk of the slug should be a slug of a personal container
$pieces = $this->explodePath($globals->getPath());
// if we don't have at least two pieces, we have a problem
if (count($pieces) < 2) {
throw new BadRequestHttpException();
}
// lookup based on this info
$site = $this->em->getRepository(PersonalContainer::class)->findRootForAccountBySlug(
$globals->getAccount(),
$pieces[1]
);
}
// if a site was not found to be attached, that is a problem as we don't know the root to work from
if ($site === null) {
throw new NotFoundHttpException(sprintf(
'No site is associated with the domain "%s".',
$domain->getHost()
));
}
// done
return $site;
}
/**
* @param FrontendGlobals $globals
* @return array|Container[]
*/
private function determineContainers(FrontendGlobals $globals)
{
// get stuff we need
$site = $globals->getSite();
// build initial array of containers, assume site is listed first (oldest to youngest)
$containers = array(
$site,
);
// make sure we account for the piece of path that has already been handled
$count = 0;
switch (true) {
case $globals->getSite() instanceof PersonalContainer:
$count += 2;
break;
case $globals->getSite() instanceof IntranetContainer:
$count += 1;
break;
}
$chopped = $this->chopPath($globals->getPath(), $count);
// get the pieces of the path
$slugs = $this->explodePath($chopped);
// loop over the remaining slugs
foreach ($slugs as $i => $slug) {
// see if we find one with this slug and the current parent
$match = $this->containerRepository()->findOneBy(array(
'parent' => $containers[$i],
'slug' => $slug,
));
// if nothing, we need to stop looping
if ($match === null) {
break;
}
// found one, add them to the set
$containers[] = $match;
}
// done, but first reverse so youngest is first in the array
return array_reverse($containers);
}
/**
* @param FrontendGlobals $globals
* @return Template
* @throws \Exception
*/
private function determineTheme(FrontendGlobals $globals)
{
$container = $globals->getContainer();
if ($container instanceof PersonalContainer) {
// get the root level personal container
while ( ! empty($container->getParent())) {
$container = $container->getParent();
}
// TODO: this assumes a site is always attached to a public web site; causes an error if not...
if ( ! $container->getContainer()) {
throw new NotFoundHttpException();
}
$containers = $this->em->getRepository(GenericContainer::class)->findAncestors(
$container->getContainer()
);
array_push($containers, $container->getContainer());
$containers = array_reverse($containers);
} else {
$containers = $globals->getContainers();
}
$theme = $this->themeManager->effectiveTheme($containers);
if (empty($theme)) {
throw FrontendBuilderException::noThemeAssociatedWithSite($container);
}
return $theme;
}
/**
* @param FrontendGlobals $globals
* @return Package
* @throws \Exception
*/
private function determinePackage(FrontendGlobals $globals)
{
if ($globals->getTheme() === null) {
return null;
}
return $this->packageManager->getPackageForTheme($globals->getTheme());
}
/**
* @param FrontendGlobals $globals
* @return OuterLayout
*/
private function determineOuterLayout(FrontendGlobals $globals)
{
$container = $globals->getContainer();
if ($container instanceof PersonalContainer) {
// get the root level personal container
while ( ! empty($container->getParent())) {
$container = $container->getParent();
}
$containers = $this->em->getRepository(GenericContainer::class)->findAncestors(
$container->getContainer()
);
array_push($containers, $container->getContainer());
$containers = array_reverse($containers);
} else {
$containers = $globals->getContainers();
}
return $this->themeManager->effectiveOuterLayout(array_merge(
$containers,
array($globals->getTheme())
));
}
/**
* @param FrontendGlobals $globals
* @return InnerLayout
*/
private function determineInnerLayout(FrontendGlobals $globals)
{
$container = $globals->getContainer();
if ($container instanceof PersonalContainer) {
// get the root level personal container
while ( ! empty($container->getParent())) {
$container = $container->getParent();
}
$containers = $this->em->getRepository(GenericContainer::class)->findAncestors(
$container->getContainer()
);
array_push($containers, $container->getContainer());
$containers = array_reverse($containers);
} else {
$containers = $globals->getContainers();
}
return $this->themeManager->effectiveInnerLayout(array_merge(
$containers,
array($globals->getTheme())
));
}
/**
* @param FrontendGlobals $globals
* @return array
*/
private function determineNavigations(FrontendGlobals $globals)
{
if ($globals->getContainer()->getNavigation() === null) {
return [];
}
return Transcoder::fromStorage($globals->getContainer()->getNavigation());
}
/**
* @param FrontendGlobals $globals
* @return ModuleConfig|mixed
*/
private function determineModule(FrontendGlobals $globals)
{
// make sure we account for the piece of path that has already been handled
$count = 0;
$count += (count($globals->getContainers()) - 1);
switch (true) {
case $globals->getSite() instanceof PersonalContainer:
$count += 2;
break;
case $globals->getSite() instanceof IntranetContainer:
$count += 1;
break;
}
$chopped = $this->chopPath($globals->getPath(), $count);
// get the remaining slugs
$pieces = $this->explodePath($chopped);
// HACK: keep supporting the @ method to dictate modules for all current links
if (preg_match('/^@(.+)$/', $pieces[0], $matches) === 1) {
// attempt to find the module from the given string, could be bogus
try {
// found it
$module = $this->moduleManager->getModuleConfiguration($matches[1]);
// pages are weird, we should not have gotten a page module this way
if ($module instanceof PageModuleConfig) {
throw new \Exception();
}
// good to go
return $module;
} catch (\Exception $e) {
// not found, assume it is a page
return $this->moduleManager->getModuleConfiguration(PageModuleConfig::NAME);
}
} else {
// see if we are a module
if ($this->moduleManager->isModule($pieces[0])) {
// return the module then
return $this->moduleManager->getModuleConfiguration($pieces[0]);
}
// check for files
if ($pieces[0] === 'files') {
return new FileHandlerHack(
$this->em,
$this->s3,
$this->mimeHelper,
);
}
// check for notifications
if ($pieces[0] === 'notification') {
return new NotificationsHandlerHack(
$this->em,
$this->s3,
$this->mediaDecorator,
$this->twig,
$this->databaseLoader,
);
}
}
// no specific module, assume page
return $this->moduleManager->getModuleConfiguration(PageModuleConfig::NAME);
}
/**
* @param FrontendGlobals $globals
* @return string
*/
private function determineExtra(FrontendGlobals $globals)
{
// init the count
$count = 0;
// we know we need to handle containers
$count += (count($globals->getContainers()) - 1);
// if it is not the page module, we need to account for the module slug
if ( ! $globals->getModule() instanceof PageModuleConfig) {
$count += 1;
}
// if there is an account set, there are two slugs that should be accounted for
switch (true) {
case $globals->getSite() instanceof PersonalContainer:
$count += 2;
break;
case $globals->getSite() instanceof IntranetContainer:
$count += 1;
break;
}
// return based on the count
return $this->chopPath($globals->getPath(), $count);
}
/**
* @param string $path
* @return array
*/
private function explodePath($path)
{
return explode('/', trim(trim($path), '/'));
}
/**
* @param string $path
* @param int $chop
* @return string
*/
private function chopPath($path, $chop)
{
return '/' . implode('/', array_slice($this->explodePath($path), $chop));
}
/**
* @param Request $request
* @return RedirectResponse
* @throws \Exception
*/
private function handleMimic(Request $request)
{
$uid = $request->query->get('_mimic');
if ( ! preg_match('/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/', $uid)) {
throw new NotFoundHttpException();
}
$account = $this->accountRepository()->findByUid($uid);
$response = new RedirectResponse(
strtok($request->getUri(), '?'),
RedirectResponse::HTTP_FOUND,
array(
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'mimic:%s',
$uid
),
)
);
$response->headers->setCookie(
new Cookie(
self::ACCOUNT_UID_KEY,
$account->getUid()->toString(),
0,
'/',
null,
false,
false
)
);
return $response;
}
/**
* @param FrontendGlobals $globals
* @return ModuleSettings|null
*/
private function determineModuleSettings(FrontendGlobals $globals)
{
// attempt to obtain module settings class name; some may not implement this yet
try {
$settingsClass = $globals->getModule()->settingsClass();
} catch (\Exception $e) {
return null;
}
// get the repo for finding settings
/** @var ModuleSettingsRepository $repo */
$repo = $this->em->getRepository($settingsClass);
// try to get settings for container, may not find any
$settings = $repo->findOneByContainer($globals->getContainer());
// if none found, create a new empty one
if (empty($settings)) {
$settings = new $settingsClass();
}
// done
return $settings;
}
/**
* @param Domain $domain
* @return Container|null
*/
private function resolveSiteByStagingDomain(Domain $domain): ?Container
{
$regex = '/^([-a-zA-Z0-9]+)\.([-a-zA-Z0-9]+)\.' . $this->params->get('cms.container.staging.domain') . '$/';
if (preg_match($regex, $domain->getName(), $matches) !== 1) {
return null;
}
return $this->containerRepository()->findOneBy([
'parent' => null,
'slug' => $matches[1],
]);
}
}
/**
* For ability to have file endpoints.
*
* Class FileHandlerHack
* @package Cms\FrontendBundle\Service
*/
final class FileHandlerHack
{
const STREAMABLE = array(
'css',
'htm',
'html',
'js',
'xhtml',
'xml',
);
/**
* @var EntityManager
*/
private $em;
/**
* @var S3Wrapper
*/
private $s3;
/**
* @var MimeHelper
*/
private $mimeHelper;
/**
* @param EntityManager $em
* @param S3Wrapper $s3
* @param MimeHelper $mimeHelper
*/
public function __construct(EntityManager $em, S3Wrapper $s3, MimeHelper $mimeHelper)
{
$this->em = $em;
$this->s3 = $s3;
$this->mimeHelper = $mimeHelper;
}
/**
* @param FrontendGlobals $globals
* @return RedirectResponse|StreamedResponse
*/
public function frontend(FrontendGlobals $globals)
{
// break apart by slashes
$paths = array_filter(explode('/', $globals->getExtra()));
// last should be the filename
$filepath = array_pop($paths);
// determine the folder
$folder = null;
foreach ($paths as $path) {
// search for the next one
$folder = $this->em->getRepository(Folder::class)->findOneBy(array(
'container' => $globals->getContainer(),
'parent' => $folder,
'name' => $path,
));
// if we don't find one, then the path is bad, stop checking
if (empty($folder)) {
break;
}
}
// double check folder
if (empty($folder)) {
throw new NotFoundHttpException();
}
// break apart the filename
$fileparts = explode('.', $filepath);
$fileext = (count($fileparts) > 0) ? array_pop($fileparts) : null;
$filename = implode('.', $fileparts);
// made it this far, have the final folder, get the file
$file = $this->em->getRepository(File::class)->findOneBy([
'container' => $globals->getContainer(),
'parent' => $folder,
'name' => $filename,
'extension' => $fileext,
]);
// double check file
if (empty($file)) {
throw new NotFoundHttpException();
}
// handle special cases
$redir = null;
switch (true) {
// passing through size width and height; need to match to predefined constant
case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('size')):
$mask = array_search(
$globals->getRequest()->query->get('size'),
ImageOptimization::$sizes
);
if ( ! empty($mask)) {
$redir = $this->s3->entityUrl(
S3Wrapper::BUCKETS__STORAGE,
$file,
sprintf(
'/optimizations/%s',
$mask
)
);
}
break;
// passing through a mask integer bitmask
case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('mask')):
$mask = $globals->getRequest()->query->get('mask');
if (array_key_exists($mask, ImageOptimization::$sizes)) {
$redir = $this->s3->entityUrl(
S3Wrapper::BUCKETS__STORAGE,
$file,
sprintf(
'/optimizations/%s',
$mask
)
);
}
break;
// passing through a constant name, like MASKS__*; only pass in the * part
case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('const')):
$const = 'MASKS__' . $globals->getRequest()->query->get('const');
$const = ImageOptimization::class . '::' . $const;
if (defined($const)) {
$redir = $this->s3->entityUrl(
S3Wrapper::BUCKETS__STORAGE,
$file,
sprintf(
'/optimizations/%s',
constant($const)
)
);
}
break;
// streaming of file types that have issues when redirected, like html, css, etc
case in_array($file->getExtension(), self::STREAMABLE):
return $this->stream($file);
}
// do default if none handled so far
if (empty($redir)) {
$redir = $this->s3->entityUrl(
S3Wrapper::BUCKETS__STORAGE,
$file,
sprintf(
'/file/%s',
$file->getFilename()
)
);
}
// redirect to s3 location
return new RedirectResponse(
$redir,
RedirectResponse::HTTP_FOUND,
array(
'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
'file:%s',
$file->getId()
),
)
);
}
/**
* @param File $file
* @return StreamedResponse
* @throws \Exception
*/
private function stream(File $file)
{
// generate the path, use streaming php prefix
$path = $this->s3->getFileStreamingPath(
S3Wrapper::BUCKETS__STORAGE,
$file,
sprintf(
'/file/%s',
$file->getFilename()
)
);
// detect mime type
$mime = $this->mimeHelper->determineMime($file->getFilename());
// create the response
$response = new StreamedResponse();
// set the headers needed
$response->headers->set('Content-Type', $mime);
// actually send the data
$response->setCallback(function () use ($path) {
// open a stream in read-only mode
if ($stream = fopen($path, 'r')) {
// loop while stream is open
while ( ! feof($stream)) {
// read bytes from the stream
echo fread($stream, 1024);
}
// be sure to close the stream resource when you're done with it
fclose($stream);
}
});
// send back the response
return $response;
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @throws \Exception
*/
public function settingsClass()
{
throw new \Exception();
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return null
*/
public function name()
{
return 'Files';
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return null
*/
public function key()
{
return 'files';
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return array
*/
public function types()
{
return array(
'File',
);
}
/**
* Whether the current department is to be powered by newer feed/object data.
*
* @param Container|Tenant $thing
* @return bool
*/
public function isSchoolNow($thing): bool
{
if ($thing instanceof Tenant) {
return $thing->isSchoolNow();
}
if ( ! $thing instanceof Container) {
throw new \Exception();
}
return $thing->isSchoolNow();
}
}
/**
* For ability to have notifications endpoints.
*
* Class NotificationsHandlerHack
* @package Cms\FrontendBundle\Service
*/
final class NotificationsHandlerHack
{
use SocialMetaTrait;
/**
* @var EntityManager
*/
private EntityManager $em;
/**
* @var S3Wrapper
*/
private S3Wrapper $s3;
/**
* @param EntityManager $em
* @param S3Wrapper $s3
*/
public function __construct(
EntityManager $em,
S3Wrapper $s3,
MediaDecorator $mediaDecorator,
Environment $twig,
DatabaseLoader $databaseLoader
)
{
$this->em = $em;
$this->s3 = $s3;
$this->mediaDecorator = $mediaDecorator;
$this->twig = $twig;
$this->databaseLoader = $databaseLoader;
}
/**
* @param FrontendGlobals $globals
* @return array|RedirectResponse
*/
public function frontend(FrontendGlobals $globals)
{
// break apart by slashes
$paths = array_values(array_filter(explode('/', $globals->getExtra())));
// if ther are no paths, then we have a problem (must have an id)
if (empty($paths)) {
throw new NotFoundHttpException();
}
// grab the id of the message and look it up in the db
$message = $this->em->getRepository(Message::class)->find($paths[0]);
if (empty($message)) {
throw new NotFoundHttpException();
}
// grab the slug
$slug = $paths[1];
// check slugs, and if incorrect, redirect to proper url with slug
if (Slugger::slug($message->getTitle()) !== $slug) {
return new RedirectResponse(sprintf(
'%s/%s/%s/%s',
$globals->pathPrefix(),
$globals->getModule()->key(),
$message->getId(),
Slugger::slug($message->getTitle())
));
}
// set the thing on the globals
//$globals->setThing($message);
// fill in media urls
if ($message->getMedia()) {
$this->mediaDecorator->urls($message->getMedia());
}
// set social meta tags
$this->setSocialMeta(
$globals,
[
'title' => $message->getTitle(),
'description' => strip_tags($message->getDescription()),
// @TODO refactor to $message->getMedia()
'cs:image-presized' => ( ! empty($message->getMedia())),
'image' => $message->getMedia(),
]
);
// set title
$globals->getAssetsOrganizer()->getTitles()
->add(new TitleStructure($message->getTitle()))
->add(new TitleStructure($globals->getContainer()->getName()));
// render the content
$content = $this->twig->render(
$this->databaseLoader->determine(
$globals->getTheme(),
sprintf(
'/modules/%s/View/build/tpl.html.twig',
$globals->getModule()->name()
)
),
[
'globals' => $globals,
'item' => $message,
]
);
// pass back as we need it
return [
[
'content' => [
(new Html())
->setContent($content),
],
],
];
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @throws \Exception
*/
public function settingsClass()
{
throw new \Exception();
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return string
*/
public function name(): string
{
return 'Notifications';
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return string
*/
public function key(): string
{
return 'notification';
}
/**
* Needed for ModuleConfig usage in calling code.
*
* @return array
*/
public function types(): array
{
return [
'Message',
];
}
/**
* Whether the current department is to be powered by newer feed/object data.
*
* @param Container|Tenant $thing
* @return bool
*/
public function isSchoolNow($thing): bool
{
if ($thing instanceof Tenant) {
return $thing->isSchoolNow();
}
if ( ! $thing instanceof Container) {
throw new \Exception();
}
return $thing->isSchoolNow();
}
}