src/Cms/FrontendBundle/Service/FrontendBuilder.php line 1178

Open in your IDE?
  1. <?php
  2. namespace Cms\FrontendBundle\Service;
  3. use App\Service\MediaDecorator;
  4. use App\Subscriber\HstsSubscriber;
  5. use Cms\AssetsBundle\Model\Structure\MetaStructure;
  6. use Cms\AssetsBundle\Model\Structure\TitleStructure;
  7. use Cms\ContainerBundle\Doctrine\ContainerRepository;
  8. use Cms\ContainerBundle\Doctrine\Containers\IntranetContainerRepository;
  9. use Cms\ContainerBundle\Entity\Container;
  10. use Cms\ContainerBundle\Entity\Containers\GenericContainer;
  11. use Cms\ContainerBundle\Entity\Containers\IntranetContainer;
  12. use Cms\ContainerBundle\Entity\Containers\PersonalContainer;
  13. use Cms\ContainerBundle\Service\ContainerService;
  14. use Cms\ContentBundle\Service\FrontendRenderer;
  15. use Cms\CoreBundle\Model\Interfaces\Identifiable\IdentifiableInterface;
  16. use Cms\CoreBundle\Service\Aws\S3Wrapper;
  17. use Cms\CoreBundle\Service\ContextManager;
  18. use Cms\CoreBundle\Service\SceneRenderer;
  19. use Cms\CoreBundle\Service\Slugger;
  20. use Cms\CoreBundle\Service\Transcoding\Transcoder;
  21. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  22. use Cms\DomainBundle\Doctrine\DomainRepository;
  23. use Cms\DomainBundle\Entity\Domain;
  24. use Cms\DomainBundle\Entity\SslCertificate;
  25. use Cms\FileBundle\Entity\Nodes\File;
  26. use Cms\FileBundle\Entity\Nodes\Files\ImageFile;
  27. use Cms\FileBundle\Entity\Nodes\Folder;
  28. use Cms\FileBundle\Entity\Optimizations\ImageOptimization;
  29. use Cms\FileBundle\Service\MimeHelper;
  30. use Cms\FrontendBundle\Exception\FrontendBuilderException;
  31. use Cms\FrontendBundle\Model\FrontendGlobals;
  32. use Cms\ModuleBundle\Doctrine\ModuleSettingsRepository;
  33. use Cms\ModuleBundle\Entity\Draft;
  34. use Cms\ModuleBundle\Entity\History;
  35. use Cms\ModuleBundle\Entity\ModuleEntity;
  36. use Cms\ModuleBundle\Entity\ModuleSettings;
  37. use Cms\ModuleBundle\Entity\Proxy;
  38. use Cms\ModuleBundle\Entity\Revision;
  39. use Cms\ModuleBundle\Model\ModuleConfig;
  40. use Cms\ModuleBundle\Model\Traits\FrontendActions\SocialMetaTrait;
  41. use Cms\ModuleBundle\Service\PublicationService;
  42. use Cms\ModuleBundle\Service\ModuleManager;
  43. use Cms\ModuleBundle\Util\ModuleProxyPlaceholderDoctrineFilter;
  44. use Cms\Modules\PageBundle\Service\PageModuleConfig;
  45. use Cms\RedirectBundle\Doctrine\RedirectRepository;
  46. use Cms\RedirectBundle\Entity\Redirect;
  47. use Cms\TenantBundle\Entity\Tenant;
  48. use Cms\TenantBundle\Model\ProductsBitwise;
  49. use Cms\ThemeBundle\Entity\InnerLayout;
  50. use Cms\ThemeBundle\Entity\OuterLayout;
  51. use Cms\ThemeBundle\Entity\Template;
  52. use Cms\ThemeBundle\Model\Package;
  53. use Cms\ThemeBundle\Service\PackageManager;
  54. use Cms\ThemeBundle\Service\ThemeManager;
  55. use Cms\ThemeBundle\Service\Twig\Loader\DatabaseLoader;
  56. use Cms\WidgetBundle\Model\WidgetObject;
  57. use Cms\Widgets\Html\Html;
  58. use Common\Util\Requests;
  59. use Platform\SecurityBundle\Doctrine\Identity\AccountRepository;
  60. use Products\NotificationsBundle\Entity\Notifications\Message;
  61. use Products\SocialBundle\Entity\SocialAccount;
  62. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  63. use Symfony\Component\HttpFoundation\Cookie;
  64. use Platform\SecurityBundle\Entity\Identity\Account;
  65. use Symfony\Component\HttpFoundation\RedirectResponse;
  66. use Symfony\Component\HttpFoundation\Request;
  67. use Symfony\Component\HttpFoundation\Response;
  68. use Symfony\Component\HttpFoundation\StreamedResponse;
  69. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  70. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  71. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  72. use Twig\Environment;
  73. /**
  74.  * Class FrontendBuilder
  75.  * @package Cms\FrontendBundle\Service
  76.  */
  77. final class FrontendBuilder
  78. {
  79.     const ACCOUNT_UID_KEY 'campussuiteAccountUid';
  80.     const QUERIES_SKIP = array(
  81.         // Google Analytics query string tracking variables
  82.         'utm_source',
  83.         'utm_medium',
  84.         'utm_term',
  85.         'utm_content',
  86.         'utm_campaign',
  87.     );
  88.     /**
  89.      * @var EntityManager
  90.      */
  91.     private EntityManager $em;
  92.     /**
  93.      * @param ParameterBagInterface $params
  94.      * @param ContainerService $containerService
  95.      * @param DatabaseLoader $databaseLoader
  96.      * @param EntityManager $em
  97.      * @param Environment $twig
  98.      * @param FrontendRenderer $frontendRenderer
  99.      * @param ModuleManager $moduleManager
  100.      * @param PackageManager $packageManager
  101.      * @param S3Wrapper $s3Wrapper
  102.      * @param SceneRenderer $sceneRenderer
  103.      * @param ThemeManager $themeManager
  104.      * @param Transcoder $transcoder
  105.      * @param ContextManager $contextManager
  106.      * @param PublicationService $publicationService
  107.      * @param MimeHelper $mimeHelper
  108.      * @param MediaDecorator $mediaDecorator
  109.      */
  110.     public function __construct(
  111.         ParameterBagInterface $params,
  112.         ContainerService $containerService,
  113.         DatabaseLoader $databaseLoader,
  114.         EntityManager $em,
  115.         Environment $twig,
  116.         FrontendRenderer $frontendRenderer,
  117.         ModuleManager $moduleManager,
  118.         PackageManager $packageManager,
  119.         S3Wrapper $s3Wrapper,
  120.         SceneRenderer $sceneRenderer,
  121.         ThemeManager $themeManager,
  122.         Transcoder $transcoder,
  123.         ContextManager $contextManager,
  124.         PublicationService $publicationService,
  125.         MimeHelper $mimeHelper,
  126.         MediaDecorator $mediaDecorator
  127.     ) {
  128.         $this->params $params;
  129.         $this->containerService $containerService;
  130.         $this->databaseLoader $databaseLoader;
  131.         $this->em $em;
  132.         $this->moduleManager $moduleManager;
  133.         $this->packageManager $packageManager;
  134.         $this->frontendRenderer $frontendRenderer;
  135.         $this->s3 $s3Wrapper;
  136.         $this->sceneRenderer $sceneRenderer;
  137.         $this->themeManager$themeManager;
  138.         $this->transcoder $transcoder;
  139.         $this->twig $twig;
  140.         $this->contextManager $contextManager;
  141.         $this->publicationService $publicationService;
  142.         $this->mimeHelper $mimeHelper;
  143.         $this->mediaDecorator $mediaDecorator;
  144.     }
  145.     /**
  146.      * @return DomainRepository
  147.      */
  148.     private function domainRepository()
  149.     {
  150.         return $this->em->getRepository(Domain::class);
  151.     }
  152.     /**
  153.      * @return RedirectRepository
  154.      */
  155.     private function redirectRepository()
  156.     {
  157.         return $this->em->getRepository(Redirect::class);
  158.     }
  159.     /**
  160.      * @return AccountRepository
  161.      */
  162.     private function accountRepository()
  163.     {
  164.         return $this->em->getRepository(Account::class);
  165.     }
  166.     /**
  167.      * @return ContainerRepository
  168.      */
  169.     private function containerRepository()
  170.     {
  171.         return $this->em->getRepository(Container::class);
  172.     }
  173.     /**
  174.      * @return IntranetContainerRepository
  175.      */
  176.     private function intranetContainerRepository()
  177.     {
  178.         return $this->em->getRepository(IntranetContainer::class);
  179.     }
  180.     /**
  181.      * @return string
  182.      */
  183.     private function determineEnvironment(): string
  184.     {
  185.         return $this->params->get('kernel.environment');
  186.     }
  187.     /**
  188.      * @return string
  189.      */
  190.     private function determineDashboard(): string
  191.     {
  192.         return $this->params->get('dashboard.hostname');
  193.     }
  194.     /**
  195.      * @return Tenant
  196.      */
  197.     private function determineTenant(): Tenant
  198.     {
  199.         $tenant $this->contextManager->getGlobalContext()->getTenant();
  200.         if ( ! $tenant) {
  201.             throw new \RuntimeException();
  202.         }
  203.         return $tenant;
  204.     }
  205.     /**
  206.      * @param ModuleEntity $entity
  207.      * @return Response
  208.      * @throws \Exception
  209.      */
  210.     public function preview(ModuleEntity $entity)
  211.     {
  212.         // determine the proxy
  213.         $proxy null;
  214.         switch (true) {
  215.             case $entity instanceof Proxy:
  216.                 $proxy $entity;
  217.                 break;
  218.             case $entity instanceof History:
  219.                 $proxy $entity->getProxy();
  220.                 break;
  221.             case $entity instanceof Draft:
  222.                 $proxy $entity->getProxy();
  223.                 break;
  224.             case $entity instanceof Revision:
  225.                 $proxy $entity->getDraft()->getProxy();
  226.                 break;
  227.             default:
  228.                 throw new \Exception();
  229.         }
  230.         // generate the url for the module entity
  231.         $url $this->publicationService->getFrontLink($proxyfalse);
  232.         // generate the preview
  233.         $preview $this->build(
  234.             Request::create($url),
  235.             function (FrontendGlobals $globals) use ($entity) {
  236.                 // set the override content to like a draft, history, or revision
  237.                 $globals->setThingOverride($entity);
  238.                 // ensure no mimic
  239.                 $globals->setMimic(null);
  240.                 // need to set the base
  241.                 $globals->getAssetsOrganizer()->getBaseTag()->set(
  242.                     'href',
  243.                     $globals->pathPrefix()
  244.                 );
  245.                 // due to how the lookup is needed to be made, need to disable our filter for placeholders
  246.                 $this->em->getFilters()
  247.                     ->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
  248.                     ->setParameter(
  249.                         'mode',
  250.                         ModuleProxyPlaceholderDoctrineFilter::MODES__ANY
  251.                     );
  252.             }
  253.         );
  254.         return $preview;
  255.     }
  256.     /**
  257.      * @param mixed|Request|FrontendGlobals $input
  258.      * @return FrontendGlobals
  259.      * @throws \Exception
  260.      */
  261.     public function globals($input)
  262.     {
  263.         // create the globals if not given
  264.         switch (true) {
  265.             case is_string($input):
  266.                 $globals = new FrontendGlobals(Requests::factory($input));
  267.                 break;
  268.             case $input instanceof FrontendGlobals:
  269.                 $globals $input;
  270.                 break;
  271.             case $input instanceof Request:
  272.                 $globals = new FrontendGlobals($input);
  273.                 break;
  274.             default:
  275.                 throw new \Exception();
  276.         }
  277.         // determine if we should do debugging
  278.         $debugging $this->contextManager->getGlobalContext()->getTenant();
  279.         $globals->debugging( ! empty($debugging) && $debugging->getDebugging() === true);
  280.         // save the environment
  281.         $globals->debugStart('setEnvironment');
  282.         $globals->setEnvironment(
  283.             $this->determineEnvironment()
  284.         );
  285.         $globals->debugStop('setEnvironment');
  286.         // obtain the tenant
  287.         $globals->debugStart('setTenant');
  288.         $globals->setTenant(
  289.             $this->determineTenant()
  290.         );
  291.         $globals->debugStop('setTenant');
  292.         // save the dashboard hostname
  293.         $globals->debugStart('setDashboard');
  294.         $globals->setDashboard(
  295.             $this->determineDashboard()
  296.         );
  297.         $globals->debugStop('setDashboard');
  298.         // obtain just the url
  299.         $globals->debugStart('setPath');
  300.         $globals->setPath(
  301.             $this->determinePath($globals)
  302.         );
  303.         $globals->debugStop('setPath');
  304.         // get the domain entity tied to this site
  305.         $globals->debugStart('setDomain');
  306.         $globals->setDomain(
  307.             $this->determineDomain($globals)
  308.         );
  309.         $globals->debugStop('setDomain');
  310.         // determine if we are running a personal site
  311.         $globals->debugStart('setAccount');
  312.         $globals->setAccount(
  313.             $this->determineAccount($globals)
  314.         );
  315.         $globals->debugStop('setAccount');
  316.         // deterime the website for this (i.e. root container)
  317.         $globals->debugStart('setSite');
  318.         $globals->setSite(
  319.             $this->determineSite($globals)
  320.         );
  321.         $globals->debugStop('setSite');
  322.         // get the path of containers to where we are at
  323.         $globals->debugStart('setContainers');
  324.         $globals->setContainers(
  325.             $this->determineContainers($globals)
  326.         );
  327.         $globals->debugStop('setContainers');
  328.         // set the specific container
  329.         $globals->debugStart('setContainer');
  330.         $globals->setContainer(
  331.             $this->determineContainer($globals)
  332.         );
  333.         $globals->debugStop('setContainer');
  334.         // get mimicking account if any
  335.         $globals->debugStart('setMimic');
  336.         $globals->setMimic(
  337.             $this->determineMimic($globals)
  338.         );
  339.         $globals->debugStop('setMimic');
  340.         // get the theme for the container we are at
  341.         $globals->debugStart('setTheme');
  342.         $globals->setTheme(
  343.             $this->determineTheme($globals)
  344.         );
  345.         $globals->debugStop('setTheme');
  346.         // get the package for the theme
  347.         $globals->debugStart('setPackage');
  348.         $globals->setPackage(
  349.             $this->determinePackage($globals)
  350.         );
  351.         $globals->debugStop('setPackage');
  352.         // go ahead and get an outer layout
  353.         $globals->debugStart('setOuterLayout');
  354.         $globals->setOuterLayout(
  355.             $this->determineOuterLayout($globals)
  356.         );
  357.         $globals->debugStop('setOuterLayout');
  358.         // go ahead and get an inner layout, this may be changed later by module specific code
  359.         $globals->debugStart('setInnerLayout');
  360.         $globals->setInnerLayout(
  361.             $this->determineInnerLayout($globals)
  362.         );
  363.         $globals->debugStop('setInnerLayout');
  364.         // build out the generated navigation
  365.         $globals->debugStart('setNavigations');
  366.         $globals->setNavigations(
  367.             $this->determineNavigations($globals)
  368.         );
  369.         $globals->debugStop('setNavigations');
  370.         // get the module type
  371.         $globals->debugStart('setModule');
  372.         $globals->setModule(
  373.             $this->determineModule($globals)
  374.         );
  375.         $globals->debugStop('setModule');
  376.         // figure out what piece of the path is left; should either be nothing or and id/slug combo
  377.         $globals->debugStart('setExtra');
  378.         $globals->setExtra(
  379.             $this->determineExtra($globals)
  380.         );
  381.         $globals->debugStop('setExtra');
  382.         // set the module settings
  383.         $globals->debugStart('setModuleSettings');
  384.         $globals->setModuleSettings(
  385.             $this->determineModuleSettings($globals)
  386.         );
  387.         $globals->debugStop('setModuleSettings');
  388.         // HACK: social support
  389.         $globals->debugStart('setSocialSupport');
  390.         $globals->setSocialSupport(
  391.             $this->determineSocialSupport($globals)
  392.         );
  393.         $globals->debugStop('setSocialSupport');
  394.         // done
  395.         return $globals;
  396.     }
  397.     /**
  398.      * @param Request $request
  399.      * @return bool
  400.      */
  401.     private function checkMimic(Request $request)
  402.     {
  403.         return $request->query->has('_mimic');
  404.     }
  405.     /**
  406.      * @param FrontendGlobals $globals
  407.      * @return bool
  408.      */
  409.     private function determineSocialSupport(FrontendGlobals $globals)
  410.     {
  411.         if ($globals->getTenant()->getProducts()->checkFlag(ProductsBitwise::SMM__BASE)) {
  412.             return ( ! empty($this->em->getRepository(SocialAccount::class)->findOneBy(array())));
  413.         }
  414.         return false;
  415.     }
  416.     /**
  417.      * @param FrontendGlobals $globals
  418.      * @return Container
  419.      */
  420.     private function determineContainer(FrontendGlobals $globals)
  421.     {
  422.         if (count($globals->getContainers()) > 0) {
  423.             return $globals->getContainers()[0];
  424.         }
  425.         return $globals->getSite();
  426.     }
  427.     /**
  428.      * @return bool
  429.      */
  430.     private function preventRedirect()
  431.     {
  432.         return ($this->contextManager->getGlobalContext()->getTenant()->getRedirects() !== true);
  433.     }
  434.     /**
  435.      * @param Request $request
  436.      * @return Redirect
  437.      */
  438.     private function checkRedirection(Request $request)
  439.     {
  440.         if ($this->preventRedirect()) {
  441.             return null;
  442.         }
  443.         // check for testing override and set host properly
  444.         $host null;
  445.         if ($request->headers->has('X-CAMPUSSUITE-REDIRECTS-CHECKER-HOSTNAME')) {
  446.             $host $request->headers->get('X-CAMPUSSUITE-REDIRECTS-CHECKER-HOSTNAME');
  447.         }
  448.         if (empty($host)) {
  449.             $host $request->getHost();
  450.         }
  451.         // get the path and break it up
  452.         $path $request->getPathInfo();
  453.         $queries $request->query->all();
  454.         // filter queries
  455.         if (count($queries)) {
  456.             $queries array_filter(
  457.                 $queries,
  458.                 function ($key) {
  459.                     return ( ! in_array($keyself::QUERIES_SKIP));
  460.                 },
  461.                 ARRAY_FILTER_USE_KEY
  462.             );
  463.         }
  464.         // add queries to path
  465.         if (count($queries)) {
  466.             $path sprintf('%s?%s'$pathhttp_build_query($queries));
  467.         }
  468.         // attempt to grab the domain
  469.         $domain $this->domainRepository()->findOneByHost($host);
  470.         // if no domain, no redirects to check
  471.         if (empty($domain)) {
  472.             return null;
  473.         }
  474.         // check static redirect first
  475.         // TODO: need to support query string in source urls; so we can support redirecting things like "/page.asp?id=1234" which is a must
  476.         $redirect $this->redirectRepository()->findActiveStaticRedirect($domain$path);
  477.         if ( ! empty($redirect)) {
  478.             return $redirect;
  479.         }
  480.         // check against wildcard redirects
  481.         $redirect $this->redirectRepository()->findActiveWildcardRedirect($domain$path);
  482.         if ( ! empty($redirect)) {
  483.             return $redirect;
  484.         }
  485.         // check for uppercase symbols in path
  486.         // if there are any, redirect to the same path, but in lower case
  487.         // this should only check the path portion of the url
  488.         // upper case should be allowed in query string
  489.         // also, need to ensure that google analytics is included in the query string still too
  490.         // with that, this code needs to use the original request information
  491.         // we also return own own redirect entity type to make code easier, as this method should return that type or null otherwise
  492.         // TODO: does this affect query params when their handling becomes more complex?
  493.         if (preg_match('/[A-Z]/'$request->getPathInfo())) {
  494.             $fixed strtolower($request->getPathInfo());
  495.             if ($request->query->count() > 0) {
  496.                 $fixed .= '?' http_build_query($request->query->all());
  497.             }
  498.             return (new Redirect())
  499.                 ->setDomain($domain)
  500.                 ->setDestination($fixed)
  501.                 ->setCode(Redirect::CODES__302)
  502.                 ->setStatus(Redirect::STATUSES__ACTIVE);
  503.         }
  504.         // no matches
  505.         return null;
  506.     }
  507.     /**
  508.      * @param Request $request
  509.      * @param Redirect $redirect
  510.      * @return RedirectResponse
  511.      * @throws \Exception
  512.      */
  513.     private function handleRedirection(Request $requestRedirect $redirect)
  514.     {
  515.         if ($this->preventRedirect()) {
  516.             return null;
  517.         }
  518.         $dest $redirect->getDestination();
  519.         $src $redirect->getSource();
  520.         // prevent circular redirects
  521.         if ($dest === $src) {
  522.             return null;
  523.         }
  524.         // detect wildcard redirects
  525.         $wildcardRegexp '/(.*)\\/\\*$/';
  526.         if (preg_match($wildcardRegexp$dest$destMatches) === && preg_match($wildcardRegexp$src$srcMatches) === 1) {
  527.             // dest and source should both end in /* now
  528.             // remove the common part from the url
  529.             if ($srcMatches[1] !== '' && mb_strpos(mb_strtolower($request->getPathInfo()), mb_strtolower($srcMatches[1])) !== 0) {
  530.                 return null;
  531.             }
  532.             // get what we need to attach to the end of the destination
  533.             $srcChunk mb_substr($request->getPathInfo(), mb_strlen($srcMatches[1]));
  534.             // do the replacement
  535.             $dest preg_replace('/\\/\\*$/', ('/' ltrim($srcChunk'/')), $dest);
  536.         }
  537.         // if no destination or it has a wrong wildcard pattern, we can't do anything
  538.         if (empty($dest) || preg_match('/\\/\\*$/'$dest) === 1) {
  539.             return null;
  540.         }
  541.         // fix the destination
  542.         if (preg_match('/^https?:\\/\\//'$dest) !== 1) {
  543.             // ensure front slash
  544.             if (substr($dest01) !== '/') {
  545.                 $dest '/' $dest;
  546.             }
  547.             // attach the original domain to it
  548.             $dest $request->getScheme() . '://' $request->getHost() . $dest;
  549.         }
  550.         // generate the base request for the redirection
  551.         // handle fragments; they get lost when parsing as a request
  552.         $parts explode('#'$dest);
  553.         $dest array_shift($parts);
  554.         $fragment = ( ! empty($parts)) ? implode('#'$parts) : null;
  555.         $base Request::create($dest);
  556.         $queries array_filter(
  557.             $request->query->all(),
  558.             function ($key) {
  559.                 return in_array($keyself::QUERIES_SKIP);
  560.             },
  561.             ARRAY_FILTER_USE_KEY
  562.         );
  563.         $queries array_merge($queries$base->query->all());
  564.         // attach query info from original request to new one
  565.         $base $base->duplicate(nullnullnullnullnullarray_merge(
  566.             $base->server->all(),
  567.             array(
  568.                 // this will set the query string, so need to append the two together first
  569.                 // TODO: do we need setting to toggle adding incoming query string or not?
  570.                 // TODO: need to change the order these get appended; some systems require them in order
  571.                 'QUERY_STRING' => http_build_query($queries),
  572.             )
  573.         ));
  574.         // get final string, and add fragment if relevant
  575.         $base $base->getUri();
  576.         if ( ! empty($fragment)) {
  577.             $base .= '#' $fragment;
  578.         }
  579.         // generate the actual redirect response
  580.         return new RedirectResponse(
  581.             $base,
  582.             $redirect->getRealCode(),
  583.             array(
  584.                 'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  585.                     'redirect:%s',
  586.                     $redirect->getId()
  587.                 ),
  588.             )
  589.         );
  590.     }
  591.     /**
  592.      * @param Request $request
  593.      * @param Domain $domain
  594.      *
  595.      * @return RedirectResponse|null
  596.      */
  597.     private function checkDomainBehaviorRedirect(Request $requestDomain $domain)
  598.     {
  599.         if (empty($domain->getId())) {
  600.             if ( ! $site $this->resolveSiteByStagingDomain($domain)) {
  601.                 return null;
  602.             }
  603.             if ( ! $site->getDomain()) {
  604.                 return null;
  605.             }
  606.             if ($request->getHost() === $site->getDomain()->getHost()) {
  607.                 return null;
  608.             }
  609.             return new RedirectResponse(
  610.                 $request->getScheme() . '://' $site->getDomain()->getHost() . $request->getRequestUri(),
  611.                 RedirectResponse::HTTP_FOUND,
  612.                 [
  613.                     'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  614.                         'domain:%s',
  615.                         $domain->getId()
  616.                     ),
  617.                 ]
  618.             );
  619.         }
  620.         // if not secure coming in, but needs to be
  621.         if ($domain instanceof Domain && $domain->getCertificate() instanceof SslCertificate && $domain->isHttpsUpgrade() && ! $request->isSecure()) {
  622.             return new RedirectResponse(
  623.                 'https://' $request->getHost() . $request->getRequestUri(),
  624.                 RedirectResponse::HTTP_FOUND,
  625.                 array(
  626.                     'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  627.                         'ssl:%s',
  628.                         $domain->getCertificate()->getId()
  629.                     ),
  630.                 )
  631.             );
  632.         }
  633.         // by default, no redirection
  634.         return null;
  635.     }
  636.     /**
  637.      * @param Request $request
  638.      * @param FrontendGlobals $globals
  639.      *
  640.      * @return RedirectResponse|null
  641.      */
  642.     private function checkPersonalSiteSlugRedirect(Request $requestFrontendGlobals $globals)
  643.     {
  644.         // check if we are a personal site
  645.         if ($globals->getSite() instanceof PersonalContainer) {
  646.             // generate the slug that we should be
  647.             $expected '~'.strtolower($globals->getAccount()->getEmail());
  648.             // obtain the slug from the front url pattern
  649.             $actual explode('/'trim(strtolower($globals->getPath()), '/'))[0];
  650.             if ($actual !== $expected) {
  651.                 return new RedirectResponse(
  652.                     rtrim(sprintf(
  653.                         '%s://%s/%s?%s',
  654.                         $request->getScheme(),
  655.                         $request->getHost(),
  656.                         str_replace($actual$expectedtrim(strtolower($request->getPathInfo()), '/')),
  657.                         $request->getQueryString()
  658.                     ), '?'),
  659.                     RedirectResponse::HTTP_FOUND,
  660.                     array(
  661.                         'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  662.                             'personal:%s',
  663.                             $globals->getAccount()->getUid()
  664.                         ),
  665.                     )
  666.                 );
  667.             }
  668.         }
  669.         // by default, no redirection
  670.         return null;
  671.     }
  672.     /**
  673.      * @param FrontendGlobals $globals
  674.      * @param array|WidgetObject[] $widgets
  675.      * @return string
  676.      */
  677.     public function renderCustom(FrontendGlobals $globals, array $widgets): string
  678.     {
  679.         return $this->render($globals, [
  680.             'content' => $widgets,
  681.         ]);
  682.     }
  683.     /**
  684.      * @param Request $request
  685.      * @param callable|null $callback
  686.      * @throws FrontendBuilderException
  687.      * @throws \Exception
  688.      * @return Response
  689.      */
  690.     public function build(Request $request, callable $callback null)
  691.     {
  692.         return $this->secureWithHSTS(
  693.             $request,
  694.             $this->buildWithoutHSTS($request$callback)
  695.         );
  696.     }
  697.     /**
  698.      * @param Request $request
  699.      * @param callable|null $callback
  700.      * @throws FrontendBuilderException
  701.      * @throws \Exception
  702.      * @return Response
  703.      */
  704.     private function buildWithoutHSTS(Request $request, callable $callback null): Response
  705.     {
  706.         // see if we need to trigger mimic functionality and do that if so
  707.         if ($this->checkMimic($request)) {
  708.             return $this->handleMimic($request);
  709.         }
  710.         // see if we need to do a redirect
  711.         if ( ! empty($redirect $this->checkRedirection($request))) {
  712.             $response $this->handleRedirection($request$redirect);
  713.             if ( ! empty($response)) {
  714.                 return $response;
  715.             }
  716.         }
  717.         // enable the filter for keeping out placeholder proxies
  718.         $this->em->getFilters()->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
  719.             ->setParameter('mode'ModuleProxyPlaceholderDoctrineFilter::MODES__NO_PLACEHOLDERS);
  720.         $this->em->getFilters()->enable(ModuleProxyPlaceholderDoctrineFilter::FILTER);
  721.         // obtain globals
  722.         $globals $this->globals($request);
  723.         // run callback if one
  724.         if (is_callable($callback)) {
  725.             call_user_func($callback$globals);
  726.         }
  727.         // ensure that request schema matches the domains behavior setting if it is set
  728.         $globals->debugStart('checkDomainBehaviorRedirect');
  729.         $response $this->checkDomainBehaviorRedirect($request$globals->getDomain());
  730.         if ( ! empty($response)) {
  731.             return $response;
  732.         }
  733.         $globals->debugStop('checkDomainBehaviorRedirect');
  734.         // if doing a teacher site, make sure the incoming url matches the proper email address
  735.         $globals->debugStart('checkPersonalSiteSlugRedirect');
  736.         $response $this->checkPersonalSiteSlugRedirect($request$globals);
  737.         if ( ! empty($response)) {
  738.             return $response;
  739.         }
  740.         $globals->debugStop('checkPersonalSiteSlugRedirect');
  741.         // run the controller for the module to get a response
  742.         $response null;
  743.         $globals->debugStart('runController');
  744.         $response $this->runController($globals);
  745.         $globals->debugStop('runController');
  746.         // TODO: is this needed anymore?
  747.         $response->headers->setCookie(
  748.             new Cookie(
  749.                 'campussuite.tenant.id',
  750.                 $globals->getTenant()->getId(),
  751.                 0,
  752.                 '/',
  753.                 null,
  754.                 false,
  755.                 false
  756.             )
  757.         );
  758.         // done
  759.         return $response;
  760.     }
  761.     /**
  762.      * @param Request $request
  763.      * @param Response $response
  764.      * @return Response
  765.      * @throws \Exception
  766.      */
  767.     private function secureWithHSTS(Request $requestResponse $response): Response
  768.     {
  769.         $globals $this->globals($request);
  770.         $domain $globals->getDomain();
  771.         if ( ! $domain instanceof Domain) {
  772.             return $response;
  773.         }
  774.         $certificate $domain->getCertificate();
  775.         if ( ! $domain->getCertificate() instanceof SslCertificate) {
  776.             return $response;
  777.         }
  778.         $certificateAge = (time() - $certificate->getCreatedAt()->getTimestamp());
  779.         $isCertificateMoreThanDayOld = ((24 60 60) < $certificateAge);
  780.         if ( ! $isCertificateMoreThanDayOld) {
  781.             return $response;
  782.         }
  783.         $maxAge HstsSubscriber::HSTS_MAX_AGE;
  784.         $response->headers->add(['Strict-Transport-Security' => "max-age=$maxAge"]);
  785.         return $response;
  786.     }
  787.     /**
  788.      * @param FrontendGlobals $globals
  789.      * @return string
  790.      */
  791.     private function determinePath(FrontendGlobals $globals)
  792.     {
  793.         return rawurldecode($globals->getRequest()->getPathInfo());
  794.     }
  795.     /**
  796.      * @param FrontendGlobals $globals
  797.      * @return Account
  798.      * @throws \Exception
  799.      */
  800.     private function determineMimic(FrontendGlobals $globals)
  801.     {
  802.         // handling of this depends on the type of container/site we are in
  803.         switch (true) {
  804.             case $globals->getSite() instanceof IntranetContainer:
  805.                 // we are still logged in and on the dashboard, pull from the context
  806.                 return $this->contextManager->getGlobalContext()->getEffectiveAccount();
  807.             default:
  808.                 // obtain the uid of the mimic user from cookies
  809.                 $uid $globals->getRequest()->cookies->get(self::ACCOUNT_UID_KEY);
  810.                 // if not null, find the account
  811.                 if ( ! empty($uid)) {
  812.                     return $this->accountRepository()->findExactByUid($uid);
  813.                 }
  814.         }
  815.         // no mimic
  816.         return null;
  817.     }
  818.     /**
  819.      * @param FrontendGlobals $globals
  820.      * @return Account
  821.      * @throws \Exception
  822.      */
  823.     private function determineAccount(FrontendGlobals $globals)
  824.     {
  825.         // get the slugs
  826.         $pieces $this->explodePath($globals->getPath());
  827.         // see if we match the regex, if not, we are not running a personal site
  828.         if (preg_match('/^[~](.+)$/'$pieces[0], $matches) !== 1
  829.             || ($globals->getDomain()->getHost()
  830.             === $this->contextManager->getGlobalContext()->getDashboard())
  831.         ) {
  832.             return null;
  833.         }
  834.         // what is left should be a username
  835.         // need to perform a lookup
  836.         $account $this->accountRepository()->findByEmailWithPartialFallback($matches[1]);
  837.         // if we get nothing, then that means somebody is requesting a teacher site that does not exist
  838.         if (count($account) !== || ! $account['0'] instanceof Account) {
  839.             throw new BadRequestHttpException();
  840.         }
  841.         // done
  842.         return $account['0'];
  843.     }
  844.     /**
  845.      * @param FrontendGlobals $globals
  846.      * @return Response
  847.      * @throws \Exception
  848.      */
  849.     private function runController(FrontendGlobals $globals)
  850.     {
  851.         // get the module config
  852.         $module $globals->getModule();
  853.         $container $globals->getContainer();
  854.         // try to run the controller
  855.         try {
  856.             // if module is inactive, may need to throw a not found exception
  857.             $active $this->containerService->isModuleActive($container$module->name());
  858.             // do the processing
  859.             $globals->debugStart('runControllerFrontend');
  860.             $result $module->frontend($globals);
  861.             $globals->debugStop('runControllerFrontend');
  862.             // HACK: for special widget cases
  863.             // @see https://campussuiteteam.slack.com/archives/C0ZRTD8MS/p1533838566000422
  864.             // if the container on an item is not the same, check it for module enabling
  865.             $thing $globals->getThing();
  866.             if ( ! $active && $thing instanceof Proxy && $thing->getContainer() !== $container) {
  867.                 $active $this->containerService->isModuleActive($thing->getContainer(), $module->name());
  868.             }
  869.             if ( ! $active) {
  870.                 throw new NotFoundHttpException();
  871.             }
  872.             // common metadata about content
  873.             try {
  874.                 if ($globals->getTenant() instanceof Tenant) {
  875.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  876.                         'campussuite:tenant',
  877.                         $globals->getTenant()->getId()
  878.                     ));
  879.                 }
  880.                 if ($globals->getSite() instanceof Container) {
  881.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  882.                         'campussuite:site',
  883.                         $globals->getSite()->getId()
  884.                     ));
  885.                 }
  886.                 if ($globals->getContainer() instanceof Container) {
  887.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  888.                         'campussuite:department',
  889.                         $globals->getContainer()->getId()
  890.                     ));
  891.                 }
  892.                 if ($globals->getTheme() instanceof Template) {
  893.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  894.                         'campussuite:theme',
  895.                         $globals->getTheme()->getId()
  896.                     ));
  897.                 }
  898.                 if ($globals->getOuterLayout() instanceof OuterLayout) {
  899.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  900.                         'campussuite:outer_layout',
  901.                         $globals->getOuterLayout()->getId()
  902.                     ));
  903.                 }
  904.                 if ($globals->getInnerLayout() instanceof InnerLayout) {
  905.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  906.                         'campussuite:inner_layout',
  907.                         $globals->getInnerLayout()->getId()
  908.                     ));
  909.                 }
  910.                 if ($globals->getModule() instanceof ModuleConfig) {
  911.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  912.                         'campussuite:module',
  913.                         $globals->getModule()->key()
  914.                     ));
  915.                 }
  916.                 if ($globals->getThing() instanceof IdentifiableInterface) {
  917.                     $globals->getAssetsOrganizer()->getMetas()->add(new MetaStructure(
  918.                         'campussuite:content',
  919.                         $globals->getThing()->getId()
  920.                     ));
  921.                 }
  922.             } catch (\Exception $e) {
  923.                 // noop
  924.             }
  925.             // if we were returned an exception, throw it
  926.             if ($result instanceof \Exception) {
  927.                 throw $result;
  928.             }
  929.             // if we were given a response directly, return it
  930.             if ($result instanceof Response) {
  931.                 return $result;
  932.             }
  933.             // if we don't get the expected return back, throw error
  934.             if ( ! is_array($result)) {
  935.                 throw new \Exception();
  936.             }
  937.             // default stuff just to be safe
  938.             $result $result + array(
  939.                 [],
  940.                 new Response()
  941.             );
  942.         } catch (\Exception $e) {
  943.             // attempt to handle the exception
  944.             return $this->handleControllerException($globals$e);
  945.         }
  946.         // get the pieces from the result
  947.         /** @var array $content */
  948.         /** @var Response $response */
  949.         [$content$response] = $result;
  950.         // now that the main logic has been completed, we can ensure that non-live data is not returned again
  951.         $this->em->getFilters()->getFilter(ModuleProxyPlaceholderDoctrineFilter::FILTER)
  952.             ->setParameter('mode'ModuleProxyPlaceholderDoctrineFilter::MODES__NO_PLACEHOLDERS);
  953.         $this->em->getFilters()->enable(ModuleProxyPlaceholderDoctrineFilter::FILTER);
  954.         // render and return
  955.         $globals->debugStart('runControllerRender');
  956.         $rendered $this->render($globals$content);
  957.         $globals->debugStop('runControllerRender');
  958.         // fix the content on the response and set proper headers
  959.         $response
  960.             ->setContent($rendered)
  961.             ->setStatusCode(200)
  962.             ->headers->set('Content-Type''text/html')
  963.         ;
  964.         // done
  965.         return $response;
  966.     }
  967.     /**
  968.      * @param FrontendGlobals $globals
  969.      * @param \Exception $e
  970.      * @return Response
  971.      * @throws \Exception
  972.      */
  973.     private function handleControllerException(FrontendGlobals $globals\Exception $e)
  974.     {
  975.         // determine the code
  976.         switch (true) {
  977.             case $e instanceof NotFoundHttpException:
  978.                 $globals->getAssetsOrganizer()->getTitles()->add(
  979.                     new TitleStructure('404 Not Found')
  980.                 );
  981.                 $code 404;
  982.                 break;
  983.             case $e instanceof AccessDeniedHttpException:
  984.                 $globals->getAssetsOrganizer()->getTitles()->add(
  985.                     new TitleStructure('403 Forbidden')
  986.                 );
  987.                 $code 403;
  988.                 break;
  989.             case $e instanceof BadRequestHttpException:
  990.                 return new Response(null400, []);
  991.             default:
  992.                 $globals->getAssetsOrganizer()->getTitles()->add(
  993.                     new TitleStructure('500 Server Error')
  994.                 );
  995.                 $code 500;
  996.         }
  997.         // render the page content
  998.         $rendered $this->render($globals, array(
  999.             'content' => array(
  1000.                 (new Html())
  1001.                     ->setContent($this->twig->render(
  1002.                         $this->databaseLoader->determine(
  1003.                             $globals->getTheme(),
  1004.                             sprintf(
  1005.                                 '/system/http/%s/build/tpl.html.twig',
  1006.                                 $code
  1007.                             )
  1008.                         ),
  1009.                         array(
  1010.                             'code' => $code,
  1011.                             'error' => $e,
  1012.                             '_globals' => $globals->asArray(),
  1013.                         )
  1014.                     ))
  1015.             ),
  1016.         ));
  1017.         // send back a response
  1018.         return (new Response(
  1019.             $rendered,
  1020.             $code,
  1021.             array(
  1022.                 'Content-Type' => 'text/html'
  1023.             )
  1024.         ));
  1025.     }
  1026.     /**
  1027.      * @param FrontendGlobals $globals
  1028.      * @param mixed $content
  1029.      * @return string
  1030.      */
  1031.     private function render(FrontendGlobals $globals$content): string
  1032.     {
  1033.         if ( ! is_array($content)) {
  1034.             $content = [];
  1035.         }
  1036.         $this->frontendRenderer->prepare($globalsFrontendRenderer::MODES__LIVE);
  1037.         return $this->frontendRenderer->renderPage($content);
  1038.     }
  1039.     /**
  1040.      * @param FrontendGlobals $globals
  1041.      * @return Domain
  1042.      * @throws \Exception
  1043.      */
  1044.     private function determineDomain(FrontendGlobals $globals)
  1045.     {
  1046.         // obtain the hostname
  1047.         $host $globals->getRequest()->getHost();
  1048.         // determine the domain for this hostname
  1049.         $domain $this->domainRepository()->findOneByHost($host);
  1050.         // throw error if we found one that was not configured
  1051.         if ($domain === null) {
  1052.             return (new Domain())
  1053.                 ->setName($host);
  1054.         }
  1055.         // now we need the apex
  1056.         $apex $domain->getApex();
  1057.         // if the apex is not verified, we have a problem
  1058.         /*
  1059.         if ( ! $apex->getVerification()->isVerified()) {
  1060.             throw FrontendBuilderException::apexNotVerified($apex);
  1061.         }
  1062.         */
  1063.         // all is good, send back the domain, the apex is already loaded and linked to it
  1064.         return $domain;
  1065.     }
  1066.     /**
  1067.      * @param FrontendGlobals $globals
  1068.      * @return Container
  1069.      * @throws \Exception
  1070.      */
  1071.     private function determineSite(FrontendGlobals $globals)
  1072.     {
  1073.         // obtain the domain
  1074.         $domain $globals->getDomain();
  1075.         // init the site
  1076.         $site null;
  1077.         // handle based on the domain type
  1078.         switch (true) {
  1079.             case ! empty($domain->getId()):
  1080.                 // get basic container
  1081.                 $site $this->em->getRepository(GenericContainer::class)->findOneByDomain($domain);
  1082.                 if ( ! $site) {
  1083.                     throw new NotFoundHttpException(sprintf(
  1084.                         'No site is linked to the domain "%s".',
  1085.                         $domain->getHost()
  1086.                     ));
  1087.                 }
  1088.                 break;
  1089.             case empty($domain->getId()):
  1090.                 $site $this->resolveSiteByStagingDomain($domain);
  1091.                 // handle intranets
  1092.                 if ($domain->getHost() === $this->contextManager->getGlobalContext()->getDashboard()) {
  1093.                     // in this case, the next chunk of the slug should be a slug of an intranet container
  1094.                     $pieces $this->explodePath($globals->getPath());
  1095.                     // if we don't have at least one pieces, we have a problem
  1096.                     if (count($pieces) < 1) {
  1097.                         throw new BadRequestHttpException();
  1098.                     }
  1099.                     // lookup based on this info
  1100.                     $site $this->intranetContainerRepository()->findOneBy(array(
  1101.                         'parent' => null,
  1102.                         'slug' => ltrim($pieces[0], '~'),
  1103.                     ));
  1104.                 }
  1105.                 break;
  1106.             default:
  1107.                 throw FrontendBuilderException::domainUnhandled($domain);
  1108.         }
  1109.         // if we are doing a personal site, need to possibly tweak this
  1110.         if ($globals->getAccount() !== null) {
  1111.             // in this case, the next chunk of the slug should be a slug of a personal container
  1112.             $pieces $this->explodePath($globals->getPath());
  1113.             // if we don't have at least two pieces, we have a problem
  1114.             if (count($pieces) < 2) {
  1115.                 throw new BadRequestHttpException();
  1116.             }
  1117.             // lookup based on this info
  1118.             $site $this->em->getRepository(PersonalContainer::class)->findRootForAccountBySlug(
  1119.                 $globals->getAccount(),
  1120.                 $pieces[1]
  1121.             );
  1122.         }
  1123.         // if a site was not found to be attached, that is a problem as we don't know the root to work from
  1124.         if ($site === null) {
  1125.             throw new NotFoundHttpException(sprintf(
  1126.                 'No site is associated with the domain "%s".',
  1127.                 $domain->getHost()
  1128.             ));
  1129.         }
  1130.         // done
  1131.         return $site;
  1132.     }
  1133.     /**
  1134.      * @param FrontendGlobals $globals
  1135.      * @return array|Container[]
  1136.      */
  1137.     private function determineContainers(FrontendGlobals $globals)
  1138.     {
  1139.         // get stuff we need
  1140.         $site $globals->getSite();
  1141.         // build initial array of containers, assume site is listed first (oldest to youngest)
  1142.         $containers = array(
  1143.             $site,
  1144.         );
  1145.         // make sure we account for the piece of path that has already been handled
  1146.         $count 0;
  1147.         switch (true) {
  1148.             case $globals->getSite() instanceof PersonalContainer:
  1149.                 $count += 2;
  1150.                 break;
  1151.             case $globals->getSite() instanceof IntranetContainer:
  1152.                 $count += 1;
  1153.                 break;
  1154.         }
  1155.         $chopped $this->chopPath($globals->getPath(), $count);
  1156.         // get the pieces of the path
  1157.         $slugs $this->explodePath($chopped);
  1158.         // loop over the remaining slugs
  1159.         foreach ($slugs as $i => $slug) {
  1160.             // see if we find one with this slug and the current parent
  1161.             $match $this->containerRepository()->findOneBy(array(
  1162.                 'parent' => $containers[$i],
  1163.                 'slug' => $slug,
  1164.             ));
  1165.             // if nothing, we need to stop looping
  1166.             if ($match === null) {
  1167.                 break;
  1168.             }
  1169.             // found one, add them to the set
  1170.             $containers[] = $match;
  1171.         }
  1172.         // done, but first reverse so youngest is first in the array
  1173.         return array_reverse($containers);
  1174.     }
  1175.     /**
  1176.      * @param FrontendGlobals $globals
  1177.      * @return Template
  1178.      * @throws \Exception
  1179.      */
  1180.     private function determineTheme(FrontendGlobals $globals)
  1181.     {
  1182.         $container $globals->getContainer();
  1183.         if ($container instanceof PersonalContainer) {
  1184.             // get the root level personal container
  1185.             while ( ! empty($container->getParent())) {
  1186.                 $container $container->getParent();
  1187.             }
  1188.             // TODO: this assumes a site is always attached to a public web site; causes an error if not...
  1189.             if ( ! $container->getContainer()) {
  1190.                 throw new NotFoundHttpException();
  1191.             }
  1192.             $containers $this->em->getRepository(GenericContainer::class)->findAncestors(
  1193.                 $container->getContainer()
  1194.             );
  1195.             array_push($containers$container->getContainer());
  1196.             $containers array_reverse($containers);
  1197.         } else {
  1198.             $containers $globals->getContainers();
  1199.         }
  1200.         $theme $this->themeManager->effectiveTheme($containers);
  1201.         if (empty($theme)) {
  1202.             throw FrontendBuilderException::noThemeAssociatedWithSite($container);
  1203.         }
  1204.         return $theme;
  1205.     }
  1206.     /**
  1207.      * @param FrontendGlobals $globals
  1208.      * @return Package
  1209.      * @throws \Exception
  1210.      */
  1211.     private function determinePackage(FrontendGlobals $globals)
  1212.     {
  1213.         if ($globals->getTheme() === null) {
  1214.             return null;
  1215.         }
  1216.         return $this->packageManager->getPackageForTheme($globals->getTheme());
  1217.     }
  1218.     /**
  1219.      * @param FrontendGlobals $globals
  1220.      * @return OuterLayout
  1221.      */
  1222.     private function determineOuterLayout(FrontendGlobals $globals)
  1223.     {
  1224.         $container $globals->getContainer();
  1225.         if ($container instanceof PersonalContainer) {
  1226.             // get the root level personal container
  1227.             while ( ! empty($container->getParent())) {
  1228.                 $container $container->getParent();
  1229.             }
  1230.             $containers $this->em->getRepository(GenericContainer::class)->findAncestors(
  1231.                 $container->getContainer()
  1232.             );
  1233.             array_push($containers$container->getContainer());
  1234.             $containers array_reverse($containers);
  1235.         } else {
  1236.             $containers $globals->getContainers();
  1237.         }
  1238.         return $this->themeManager->effectiveOuterLayout(array_merge(
  1239.             $containers,
  1240.             array($globals->getTheme())
  1241.         ));
  1242.     }
  1243.     /**
  1244.      * @param FrontendGlobals $globals
  1245.      * @return InnerLayout
  1246.      */
  1247.     private function determineInnerLayout(FrontendGlobals $globals)
  1248.     {
  1249.         $container $globals->getContainer();
  1250.         if ($container instanceof PersonalContainer) {
  1251.             // get the root level personal container
  1252.             while ( ! empty($container->getParent())) {
  1253.                 $container $container->getParent();
  1254.             }
  1255.             $containers $this->em->getRepository(GenericContainer::class)->findAncestors(
  1256.                 $container->getContainer()
  1257.             );
  1258.             array_push($containers$container->getContainer());
  1259.             $containers array_reverse($containers);
  1260.         } else {
  1261.             $containers $globals->getContainers();
  1262.         }
  1263.         return $this->themeManager->effectiveInnerLayout(array_merge(
  1264.             $containers,
  1265.             array($globals->getTheme())
  1266.         ));
  1267.     }
  1268.     /**
  1269.      * @param FrontendGlobals $globals
  1270.      * @return array
  1271.      */
  1272.     private function determineNavigations(FrontendGlobals $globals)
  1273.     {
  1274.         if ($globals->getContainer()->getNavigation() === null) {
  1275.             return [];
  1276.         }
  1277.         return Transcoder::fromStorage($globals->getContainer()->getNavigation());
  1278.     }
  1279.     /**
  1280.      * @param FrontendGlobals $globals
  1281.      * @return ModuleConfig|mixed
  1282.      */
  1283.     private function determineModule(FrontendGlobals $globals)
  1284.     {
  1285.         // make sure we account for the piece of path that has already been handled
  1286.         $count 0;
  1287.         $count += (count($globals->getContainers()) - 1);
  1288.         switch (true) {
  1289.             case $globals->getSite() instanceof PersonalContainer:
  1290.                 $count += 2;
  1291.                 break;
  1292.             case $globals->getSite() instanceof IntranetContainer:
  1293.                 $count += 1;
  1294.                 break;
  1295.         }
  1296.         $chopped $this->chopPath($globals->getPath(), $count);
  1297.         // get the remaining slugs
  1298.         $pieces $this->explodePath($chopped);
  1299.         // HACK: keep supporting the @ method to dictate modules for all current links
  1300.         if (preg_match('/^@(.+)$/'$pieces[0], $matches) === 1) {
  1301.             // attempt to find the module from the given string, could be bogus
  1302.             try {
  1303.                 // found it
  1304.                 $module $this->moduleManager->getModuleConfiguration($matches[1]);
  1305.                 // pages are weird, we should not have gotten a page module this way
  1306.                 if ($module instanceof PageModuleConfig) {
  1307.                     throw new \Exception();
  1308.                 }
  1309.                 // good to go
  1310.                 return $module;
  1311.             } catch (\Exception $e) {
  1312.                 // not found, assume it is a page
  1313.                 return $this->moduleManager->getModuleConfiguration(PageModuleConfig::NAME);
  1314.             }
  1315.         } else {
  1316.             // see if we are a module
  1317.             if ($this->moduleManager->isModule($pieces[0])) {
  1318.                 // return the module then
  1319.                 return $this->moduleManager->getModuleConfiguration($pieces[0]);
  1320.             }
  1321.             // check for files
  1322.             if ($pieces[0] === 'files') {
  1323.                 return new FileHandlerHack(
  1324.                     $this->em,
  1325.                     $this->s3,
  1326.                     $this->mimeHelper,
  1327.                 );
  1328.             }
  1329.             // check for notifications
  1330.             if ($pieces[0] === 'notification') {
  1331.                 return new NotificationsHandlerHack(
  1332.                     $this->em,
  1333.                     $this->s3,
  1334.                     $this->mediaDecorator,
  1335.                     $this->twig,
  1336.                     $this->databaseLoader,
  1337.                 );
  1338.             }
  1339.         }
  1340.         // no specific module, assume page
  1341.         return $this->moduleManager->getModuleConfiguration(PageModuleConfig::NAME);
  1342.     }
  1343.     /**
  1344.      * @param FrontendGlobals $globals
  1345.      * @return string
  1346.      */
  1347.     private function determineExtra(FrontendGlobals $globals)
  1348.     {
  1349.         // init the count
  1350.         $count 0;
  1351.         // we know we need to handle containers
  1352.         $count += (count($globals->getContainers()) - 1);
  1353.         // if it is not the page module, we need to account for the module slug
  1354.         if ( ! $globals->getModule() instanceof PageModuleConfig) {
  1355.             $count += 1;
  1356.         }
  1357.         // if there is an account set, there are two slugs that should be accounted for
  1358.         switch (true) {
  1359.             case $globals->getSite() instanceof PersonalContainer:
  1360.                 $count += 2;
  1361.                 break;
  1362.             case $globals->getSite() instanceof IntranetContainer:
  1363.                 $count += 1;
  1364.                 break;
  1365.         }
  1366.         // return based on the count
  1367.         return $this->chopPath($globals->getPath(), $count);
  1368.     }
  1369.     /**
  1370.      * @param string $path
  1371.      * @return array
  1372.      */
  1373.     private function explodePath($path)
  1374.     {
  1375.         return explode('/'trim(trim($path), '/'));
  1376.     }
  1377.     /**
  1378.      * @param string $path
  1379.      * @param int $chop
  1380.      * @return string
  1381.      */
  1382.     private function chopPath($path$chop)
  1383.     {
  1384.         return '/' implode('/'array_slice($this->explodePath($path), $chop));
  1385.     }
  1386.     /**
  1387.      * @param Request $request
  1388.      * @return RedirectResponse
  1389.      * @throws \Exception
  1390.      */
  1391.     private function handleMimic(Request $request)
  1392.     {
  1393.         $uid $request->query->get('_mimic');
  1394.         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)) {
  1395.             throw new NotFoundHttpException();
  1396.         }
  1397.         $account $this->accountRepository()->findByUid($uid);
  1398.         $response = new RedirectResponse(
  1399.             strtok($request->getUri(), '?'),
  1400.             RedirectResponse::HTTP_FOUND,
  1401.             array(
  1402.                 'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  1403.                     'mimic:%s',
  1404.                     $uid
  1405.                 ),
  1406.             )
  1407.         );
  1408.         $response->headers->setCookie(
  1409.             new Cookie(
  1410.                 self::ACCOUNT_UID_KEY,
  1411.                 $account->getUid()->toString(),
  1412.                 0,
  1413.                 '/',
  1414.                 null,
  1415.                 false,
  1416.                 false
  1417.             )
  1418.         );
  1419.         return $response;
  1420.     }
  1421.     /**
  1422.      * @param FrontendGlobals $globals
  1423.      * @return ModuleSettings|null
  1424.      */
  1425.     private function determineModuleSettings(FrontendGlobals $globals)
  1426.     {
  1427.         // attempt to obtain module settings class name; some may not implement this yet
  1428.         try {
  1429.             $settingsClass $globals->getModule()->settingsClass();
  1430.         } catch (\Exception $e) {
  1431.             return null;
  1432.         }
  1433.         // get the repo for finding settings
  1434.         /** @var ModuleSettingsRepository $repo */
  1435.         $repo $this->em->getRepository($settingsClass);
  1436.         // try to get settings for container, may not find any
  1437.         $settings $repo->findOneByContainer($globals->getContainer());
  1438.         // if none found, create a new empty one
  1439.         if (empty($settings)) {
  1440.             $settings = new $settingsClass();
  1441.         }
  1442.         // done
  1443.         return $settings;
  1444.     }
  1445.     /**
  1446.      * @param Domain $domain
  1447.      * @return Container|null
  1448.      */
  1449.     private function resolveSiteByStagingDomain(Domain $domain): ?Container
  1450.     {
  1451.         $regex '/^([-a-zA-Z0-9]+)\.([-a-zA-Z0-9]+)\.' $this->params->get('cms.container.staging.domain') . '$/';
  1452.         if (preg_match($regex$domain->getName(), $matches) !== 1) {
  1453.             return null;
  1454.         }
  1455.         return $this->containerRepository()->findOneBy([
  1456.             'parent' => null,
  1457.             'slug' => $matches[1],
  1458.         ]);
  1459.     }
  1460. }
  1461. /**
  1462.  * For ability to have file endpoints.
  1463.  *
  1464.  * Class FileHandlerHack
  1465.  * @package Cms\FrontendBundle\Service
  1466.  */
  1467. final class FileHandlerHack
  1468. {
  1469.     const STREAMABLE = array(
  1470.         'css',
  1471.         'htm',
  1472.         'html',
  1473.         'js',
  1474.         'xhtml',
  1475.         'xml',
  1476.     );
  1477.     /**
  1478.      * @var EntityManager
  1479.      */
  1480.     private $em;
  1481.     /**
  1482.      * @var S3Wrapper
  1483.      */
  1484.     private $s3;
  1485.     /**
  1486.      * @var MimeHelper
  1487.      */
  1488.     private $mimeHelper;
  1489.     /**
  1490.      * @param EntityManager $em
  1491.      * @param S3Wrapper $s3
  1492.      * @param MimeHelper $mimeHelper
  1493.      */
  1494.     public function __construct(EntityManager $emS3Wrapper $s3MimeHelper $mimeHelper)
  1495.     {
  1496.         $this->em $em;
  1497.         $this->s3 $s3;
  1498.         $this->mimeHelper $mimeHelper;
  1499.     }
  1500.     /**
  1501.      * @param FrontendGlobals $globals
  1502.      * @return RedirectResponse|StreamedResponse
  1503.      */
  1504.     public function frontend(FrontendGlobals $globals)
  1505.     {
  1506.         // break apart by slashes
  1507.         $paths array_filter(explode('/'$globals->getExtra()));
  1508.         // last should be the filename
  1509.         $filepath array_pop($paths);
  1510.         // determine the folder
  1511.         $folder null;
  1512.         foreach ($paths as $path) {
  1513.             // search for the next one
  1514.             $folder $this->em->getRepository(Folder::class)->findOneBy(array(
  1515.                 'container' => $globals->getContainer(),
  1516.                 'parent' => $folder,
  1517.                 'name' => $path,
  1518.             ));
  1519.             // if we don't find one, then the path is bad, stop checking
  1520.             if (empty($folder)) {
  1521.                 break;
  1522.             }
  1523.         }
  1524.         // double check folder
  1525.         if (empty($folder)) {
  1526.             throw new NotFoundHttpException();
  1527.         }
  1528.         // break apart the filename
  1529.         $fileparts explode('.'$filepath);
  1530.         $fileext = (count($fileparts) > 0) ? array_pop($fileparts) : null;
  1531.         $filename implode('.'$fileparts);
  1532.         // made it this far, have the final folder, get the file
  1533.         $file $this->em->getRepository(File::class)->findOneBy([
  1534.             'container' => $globals->getContainer(),
  1535.             'parent' => $folder,
  1536.             'name' => $filename,
  1537.             'extension' => $fileext,
  1538.         ]);
  1539.         // double check file
  1540.         if (empty($file)) {
  1541.             throw new NotFoundHttpException();
  1542.         }
  1543.         // handle special cases
  1544.         $redir null;
  1545.         switch (true) {
  1546.             // passing through size width and height; need to match to predefined constant
  1547.             case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('size')):
  1548.                 $mask array_search(
  1549.                     $globals->getRequest()->query->get('size'),
  1550.                     ImageOptimization::$sizes
  1551.                 );
  1552.                 if ( ! empty($mask)) {
  1553.                     $redir $this->s3->entityUrl(
  1554.                         S3Wrapper::BUCKETS__STORAGE,
  1555.                         $file,
  1556.                         sprintf(
  1557.                             '/optimizations/%s',
  1558.                             $mask
  1559.                         )
  1560.                     );
  1561.                 }
  1562.                 break;
  1563.             // passing through a mask integer bitmask
  1564.             case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('mask')):
  1565.                 $mask $globals->getRequest()->query->get('mask');
  1566.                 if (array_key_exists($maskImageOptimization::$sizes)) {
  1567.                     $redir $this->s3->entityUrl(
  1568.                         S3Wrapper::BUCKETS__STORAGE,
  1569.                         $file,
  1570.                         sprintf(
  1571.                             '/optimizations/%s',
  1572.                             $mask
  1573.                         )
  1574.                     );
  1575.                 }
  1576.                 break;
  1577.             // passing through a constant name, like MASKS__*; only pass in the * part
  1578.             case $file instanceof ImageFile && ! empty($globals->getRequest()->query->get('const')):
  1579.                 $const 'MASKS__' $globals->getRequest()->query->get('const');
  1580.                 $const ImageOptimization::class . '::' $const;
  1581.                 if (defined($const)) {
  1582.                     $redir $this->s3->entityUrl(
  1583.                         S3Wrapper::BUCKETS__STORAGE,
  1584.                         $file,
  1585.                         sprintf(
  1586.                             '/optimizations/%s',
  1587.                             constant($const)
  1588.                         )
  1589.                     );
  1590.                 }
  1591.                 break;
  1592.             // streaming of file types that have issues when redirected, like html, css, etc
  1593.             case in_array($file->getExtension(), self::STREAMABLE):
  1594.                 return $this->stream($file);
  1595.         }
  1596.         // do default if none handled so far
  1597.         if (empty($redir)) {
  1598.             $redir $this->s3->entityUrl(
  1599.                 S3Wrapper::BUCKETS__STORAGE,
  1600.                 $file,
  1601.                 sprintf(
  1602.                     '/file/%s',
  1603.                     $file->getFilename()
  1604.                 )
  1605.             );
  1606.         }
  1607.         // redirect to s3 location
  1608.         return new RedirectResponse(
  1609.             $redir,
  1610.             RedirectResponse::HTTP_FOUND,
  1611.             array(
  1612.                 'X-CAMPUSSUITE-FE-REDIRECT' => sprintf(
  1613.                     'file:%s',
  1614.                     $file->getId()
  1615.                 ),
  1616.             )
  1617.         );
  1618.     }
  1619.     /**
  1620.      * @param File $file
  1621.      * @return StreamedResponse
  1622.      * @throws \Exception
  1623.      */
  1624.     private function stream(File $file)
  1625.     {
  1626.         // generate the path, use streaming php prefix
  1627.         $path $this->s3->getFileStreamingPath(
  1628.             S3Wrapper::BUCKETS__STORAGE,
  1629.             $file,
  1630.             sprintf(
  1631.                 '/file/%s',
  1632.                 $file->getFilename()
  1633.             )
  1634.         );
  1635.         // detect mime type
  1636.         $mime $this->mimeHelper->determineMime($file->getFilename());
  1637.         // create the response
  1638.         $response = new StreamedResponse();
  1639.         // set the headers needed
  1640.         $response->headers->set('Content-Type'$mime);
  1641.         // actually send the data
  1642.         $response->setCallback(function () use ($path) {
  1643.             // open a stream in read-only mode
  1644.             if ($stream fopen($path'r')) {
  1645.                 // loop while stream is open
  1646.                 while ( ! feof($stream)) {
  1647.                     // read bytes from the stream
  1648.                     echo fread($stream1024);
  1649.                 }
  1650.                 // be sure to close the stream resource when you're done with it
  1651.                 fclose($stream);
  1652.             }
  1653.         });
  1654.         // send back the response
  1655.         return $response;
  1656.     }
  1657.     /**
  1658.      * Needed for ModuleConfig usage in calling code.
  1659.      *
  1660.      * @throws \Exception
  1661.      */
  1662.     public function settingsClass()
  1663.     {
  1664.         throw new \Exception();
  1665.     }
  1666.     /**
  1667.      * Needed for ModuleConfig usage in calling code.
  1668.      *
  1669.      * @return null
  1670.      */
  1671.     public function name()
  1672.     {
  1673.         return 'Files';
  1674.     }
  1675.     /**
  1676.      * Needed for ModuleConfig usage in calling code.
  1677.      *
  1678.      * @return null
  1679.      */
  1680.     public function key()
  1681.     {
  1682.         return 'files';
  1683.     }
  1684.     /**
  1685.      * Needed for ModuleConfig usage in calling code.
  1686.      *
  1687.      * @return array
  1688.      */
  1689.     public function types()
  1690.     {
  1691.         return array(
  1692.             'File',
  1693.         );
  1694.     }
  1695.     /**
  1696.      * Whether the current department is to be powered by newer feed/object data.
  1697.      *
  1698.      * @param Container|Tenant $thing
  1699.      * @return bool
  1700.      */
  1701.     public function isSchoolNow($thing): bool
  1702.     {
  1703.         if ($thing instanceof Tenant) {
  1704.             return $thing->isSchoolNow();
  1705.         }
  1706.         if ( ! $thing instanceof Container) {
  1707.             throw new \Exception();
  1708.         }
  1709.         return $thing->isSchoolNow();
  1710.     }
  1711. }
  1712. /**
  1713.  * For ability to have notifications endpoints.
  1714.  *
  1715.  * Class NotificationsHandlerHack
  1716.  * @package Cms\FrontendBundle\Service
  1717.  */
  1718. final class NotificationsHandlerHack
  1719. {
  1720.     use SocialMetaTrait;
  1721.     /**
  1722.      * @var EntityManager
  1723.      */
  1724.     private EntityManager $em;
  1725.     /**
  1726.      * @var S3Wrapper
  1727.      */
  1728.     private S3Wrapper $s3;
  1729.     /**
  1730.      * @param EntityManager $em
  1731.      * @param S3Wrapper $s3
  1732.      */
  1733.     public function __construct(
  1734.         EntityManager $em,
  1735.         S3Wrapper $s3,
  1736.         MediaDecorator $mediaDecorator,
  1737.         Environment $twig,
  1738.         DatabaseLoader $databaseLoader
  1739.     )
  1740.     {
  1741.         $this->em $em;
  1742.         $this->s3 $s3;
  1743.         $this->mediaDecorator $mediaDecorator;
  1744.         $this->twig $twig;
  1745.         $this->databaseLoader $databaseLoader;
  1746.     }
  1747.     /**
  1748.      * @param FrontendGlobals $globals
  1749.      * @return array|RedirectResponse
  1750.      */
  1751.     public function frontend(FrontendGlobals $globals)
  1752.     {
  1753.         // break apart by slashes
  1754.         $paths array_values(array_filter(explode('/'$globals->getExtra())));
  1755.         // if ther are no paths, then we have a problem (must have an id)
  1756.         if (empty($paths)) {
  1757.             throw new NotFoundHttpException();
  1758.         }
  1759.         // grab the id of the message and look it up in the db
  1760.         $message $this->em->getRepository(Message::class)->find($paths[0]);
  1761.         if (empty($message)) {
  1762.             throw new NotFoundHttpException();
  1763.         }
  1764.         // grab the slug
  1765.         $slug $paths[1];
  1766.         // check slugs, and if incorrect, redirect to proper url with slug
  1767.         if (Slugger::slug($message->getTitle()) !== $slug) {
  1768.             return new RedirectResponse(sprintf(
  1769.                 '%s/%s/%s/%s',
  1770.                 $globals->pathPrefix(),
  1771.                 $globals->getModule()->key(),
  1772.                 $message->getId(),
  1773.                 Slugger::slug($message->getTitle())
  1774.             ));
  1775.         }
  1776.         // set the thing on the globals
  1777.         //$globals->setThing($message);
  1778.         // fill in media urls
  1779.         if ($message->getMedia()) {
  1780.             $this->mediaDecorator->urls($message->getMedia());
  1781.         }
  1782.         // set social meta tags
  1783.         $this->setSocialMeta(
  1784.             $globals,
  1785.             [
  1786.                 'title' => $message->getTitle(),
  1787.                 'description' => strip_tags($message->getDescription()),
  1788.                 // @TODO refactor to $message->getMedia()
  1789.                 'cs:image-presized' => ( ! empty($message->getMedia())),
  1790.                 'image' => $message->getMedia(),
  1791.             ]
  1792.         );
  1793.         // set title
  1794.         $globals->getAssetsOrganizer()->getTitles()
  1795.             ->add(new TitleStructure($message->getTitle()))
  1796.             ->add(new TitleStructure($globals->getContainer()->getName()));
  1797.         // render the content
  1798.         $content $this->twig->render(
  1799.             $this->databaseLoader->determine(
  1800.                 $globals->getTheme(),
  1801.                 sprintf(
  1802.                     '/modules/%s/View/build/tpl.html.twig',
  1803.                     $globals->getModule()->name()
  1804.                 )
  1805.             ),
  1806.             [
  1807.                 'globals' => $globals,
  1808.                 'item' => $message,
  1809.             ]
  1810.         );
  1811.         // pass back as we need it
  1812.         return [
  1813.             [
  1814.                 'content' => [
  1815.                     (new Html())
  1816.                         ->setContent($content),
  1817.                 ],
  1818.             ],
  1819.         ];
  1820.     }
  1821.     /**
  1822.      * Needed for ModuleConfig usage in calling code.
  1823.      *
  1824.      * @throws \Exception
  1825.      */
  1826.     public function settingsClass()
  1827.     {
  1828.         throw new \Exception();
  1829.     }
  1830.     /**
  1831.      * Needed for ModuleConfig usage in calling code.
  1832.      *
  1833.      * @return string
  1834.      */
  1835.     public function name(): string
  1836.     {
  1837.         return 'Notifications';
  1838.     }
  1839.     /**
  1840.      * Needed for ModuleConfig usage in calling code.
  1841.      *
  1842.      * @return string
  1843.      */
  1844.     public function key(): string
  1845.     {
  1846.         return 'notification';
  1847.     }
  1848.     /**
  1849.      * Needed for ModuleConfig usage in calling code.
  1850.      *
  1851.      * @return array
  1852.      */
  1853.     public function types(): array
  1854.     {
  1855.         return [
  1856.             'Message',
  1857.         ];
  1858.     }
  1859.     /**
  1860.      * Whether the current department is to be powered by newer feed/object data.
  1861.      *
  1862.      * @param Container|Tenant $thing
  1863.      * @return bool
  1864.      */
  1865.     public function isSchoolNow($thing): bool
  1866.     {
  1867.         if ($thing instanceof Tenant) {
  1868.             return $thing->isSchoolNow();
  1869.         }
  1870.         if ( ! $thing instanceof Container) {
  1871.             throw new \Exception();
  1872.         }
  1873.         return $thing->isSchoolNow();
  1874.     }
  1875. }