src/Frontend/Core/Engine/Page.php line 107

Open in your IDE?
  1. <?php
  2. namespace Frontend\Core\Engine;
  3. use Common\Exception\RedirectException;
  4. use ForkCMS\App\KernelLoader;
  5. use Frontend\Core\Engine\Base\Widget as FrontendBaseWidget;
  6. use Frontend\Core\Engine\Block\ModuleExtraInterface;
  7. use Frontend\Core\Engine\Model as FrontendModel;
  8. use Frontend\Core\Header\Header;
  9. use Frontend\Core\Language\Language;
  10. use Symfony\Component\HttpFoundation\RedirectResponse;
  11. use Symfony\Component\HttpFoundation\Response;
  12. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  13. use Symfony\Component\HttpKernel\KernelInterface;
  14. use Frontend\Core\Engine\Block\ExtraInterface as FrontendBlockExtra;
  15. use Frontend\Core\Engine\Block\Widget as FrontendBlockWidget;
  16. use Backend\Core\Engine\Model as BackendModel;
  17. use Frontend\Modules\Profiles\Engine\Authentication as FrontendAuthenticationModel;
  18. use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
  19. /**
  20.  * Frontend page class, this class will handle everything on a page
  21.  */
  22. class Page extends KernelLoader
  23. {
  24.     /**
  25.      * Breadcrumb instance
  26.      *
  27.      * @var Breadcrumb
  28.      */
  29.     protected $breadcrumb;
  30.     /**
  31.      * Array of extras linked to this page
  32.      *
  33.      * @var array
  34.      */
  35.     protected $extras = [];
  36.     /**
  37.      * Footer instance
  38.      *
  39.      * @var Footer
  40.      */
  41.     protected $footer;
  42.     /**
  43.      * Header instance
  44.      *
  45.      * @var Header
  46.      */
  47.     protected $header;
  48.     /**
  49.      * Header instance
  50.      *
  51.      * @var Modals
  52.      */
  53.     protected $modals;
  54.     /**
  55.      * The current pageId
  56.      *
  57.      * @var int
  58.      */
  59.     protected $pageId;
  60.     /**
  61.      * Content of the page
  62.      *
  63.      * @var array
  64.      */
  65.     protected $record = [];
  66.     /**
  67.      * The path of the template to show
  68.      *
  69.      * @var string
  70.      */
  71.     protected $templatePath;
  72.     /**
  73.      * The statuscode
  74.      *
  75.      * @var int
  76.      */
  77.     protected $statusCode Response::HTTP_OK;
  78.     /**
  79.      * TwigTemplate instance
  80.      *
  81.      * @var TwigTemplate
  82.      */
  83.     protected $template;
  84.     /**
  85.      * URL instance
  86.      *
  87.      * @var Url
  88.      */
  89.     protected $url;
  90.     public function __construct(KernelInterface $kernel)
  91.     {
  92.         parent::__construct($kernel);
  93.         $this->getContainer()->set('page'$this);
  94.         $this->template $this->getContainer()->get('templating');
  95.         $this->url $this->getContainer()->get('url');
  96.     }
  97.     /**
  98.      * Loads the actual components on the page
  99.      */
  100.     public function load(): void
  101.     {
  102.         // set tracking cookie
  103.         Model::getVisitorId();
  104.         // create header instance
  105.         $this->header = new Header($this->getKernel());
  106.         /* @var $pageAliasUrl PageUrlAliasInterface */
  107.         $pageAliasUrl $this->get('fork.frontend.page_url_alias');
  108.         try {
  109.             $this->handlePage($pageAliasUrl->getPageId(implode('/'$this->url->getPages()), FRONTEND_LANGUAGE));
  110.         } catch (NotFoundHttpException $notFoundHttpException) {
  111.             $this->handlePage(Response::HTTP_NOT_FOUND);
  112.         } catch (InsufficientAuthenticationException $insufficientAuthenticationException) {
  113.             $this->redirectToLogin();
  114.         }
  115.     }
  116.     private function handlePage(int $pageId)
  117.     {
  118.         // get page content from pageId of the requested URL
  119.         $this->record $this->getPageContent($pageId);
  120.         /* @var $pageAliasUrl PageUrlAliasInterface */
  121.         $pageAliasUrl $this->get('fork.frontend.page_url_alias');
  122.         //redirect when url not alias and alias url is present
  123.         if(!empty($this->record) && !$pageAliasUrl->isAlias(implode('/'$this->url->getPages()), FRONTEND_LANGUAGE) && $pageAliasUrl->hasPageIdAlias($this->record['id'], FRONTEND_LANGUAGE)) {
  124.             throw new RedirectException(
  125.                 'Redirect',
  126.                 new RedirectResponse(
  127.                     Navigation::getURL($this->record['id']),
  128.                     301
  129.                 )
  130.             );
  131.         }
  132.         if (empty($this->record)) {
  133.             throw new NotFoundHttpException('No page was found for the page id:' $pageId);
  134.         }
  135.         $this->checkAuthentication();
  136.         // we need to set the correct id
  137.         $this->pageId = (int) $this->record['id'];
  138.         if ($this->pageId === Response::HTTP_NOT_FOUND) {
  139.             $this->statusCode Response::HTTP_NOT_FOUND;
  140.             if (extension_loaded('newrelic')) {
  141.                 newrelic_name_transaction('404');
  142.             }
  143.         }
  144.         $this->breadcrumb = new Breadcrumb($this->getKernel());
  145.         $this->footer = new Footer($this->getKernel());
  146.         $this->modals = new Modals($this->getKernel(), $this->pageId);
  147.         $this->processPage();
  148.         // execute all extras linked to the page
  149.         array_map([$this'processExtra'], $this->extras);
  150.     }
  151.     private function checkAuthentication(): void
  152.     {
  153.         // no authentication needed
  154.         if (!isset($this->record['data']['auth_required'])
  155.             || !$this->record['data']['auth_required']
  156.             || !BackendModel::isModuleInstalled('Profiles')
  157.         ) {
  158.             return;
  159.         }
  160.         if (!FrontendAuthenticationModel::isLoggedIn()) {
  161.             throw new InsufficientAuthenticationException('You must log in to see this page');
  162.         }
  163.         // specific groups for auth?
  164.         if (empty($this->record['data']['auth_groups'])) {
  165.             // no further checks needed
  166.             return;
  167.         }
  168.         foreach ($this->record['data']['auth_groups'] as $group) {
  169.             if (FrontendAuthenticationModel::getProfile()->isInGroup($group)) {
  170.                 // profile is in a group that is allowed to see the page
  171.                 return;
  172.             }
  173.         }
  174.         throw new NotFoundHttpException('The current page is not available to the logged in profile');
  175.     }
  176.     public function display(): Response
  177.     {
  178.         try {
  179.             $this->template->assignGlobal('homePage'$this->pageId == 1);
  180.             // assign the id so we can use it as an option
  181.             $this->template->assignGlobal('isPage' $this->pageIdtrue);
  182.             $this->template->assignGlobal('isChildOfPage' $this->record['parent_id'], true);
  183.             // hide the cookiebar from within the code to prevent flickering
  184.             $this->template->assignGlobal(
  185.                 'cookieBarHide',
  186.                 !$this->get('fork.settings')->get('Core''show_cookie_bar'false)
  187.                 || $this->getContainer()->get('fork.cookie')->hasHiddenCookieBar()
  188.             );
  189.             // assign globals for GtmCookieBar module
  190.             if (BackendModel::isModuleInstalled('GtmCookieBar'))
  191.             {
  192.                 $gtmId $this->get('fork.settings')->get('GtmCookieBar''gtmId'null);
  193.                 $gtmId = !empty($gtmId) ? (string) $gtmId null;
  194.                 $this->template->assignGlobal('gtmId'$gtmId);
  195.                 $this->template->assignGlobal('gtmCookieSettings', \Frontend\Modules\GtmCookieBar\Engine\Model::getGtmCookieSettings());
  196.                 // Kept for backwards compatibility with existing theme includes.
  197.                 $this->template->assignGlobal('cookiebar_template''/GtmCookieBar/Layout/Templates/CookieBar.html.twig');
  198.                 // Kept for backwards compatibility with existing theme includes.
  199.                 $this->template->assignGlobal('gtm_template''/GtmCookieBar/Layout/Templates/Gtm.html.twig');
  200.             }
  201.             // assign globals for PageExtend module
  202.             if (BackendModel::isModuleInstalled('PagesExtension'))
  203.             {
  204.                 $this->template->assignGlobal('pageClass', \Frontend\Modules\PagesExtension\Engine\Model::getPageClass($this->pageIdFRONTEND_LANGUAGE) ?: null);
  205.             }
  206.             $this->parsePositions();
  207.             // assign empty positions
  208.             $unusedPositions array_diff(
  209.                 $this->record['template_data']['names'],
  210.                 array_keys($this->record['positions'])
  211.             );
  212.             foreach ($unusedPositions as $position) {
  213.                 $this->template->assign('position' . \SpoonFilter::ucfirst($position), []);
  214.             }
  215.             $this->header->parse();
  216.             $this->breadcrumb->parse();
  217.             $this->parseLanguages();
  218.             $this->footer->parse();
  219.             $this->modals->parse();
  220.             return new Response(
  221.                 $this->template->getContent($this->templatePath),
  222.                 $this->statusCode
  223.             );
  224.         } catch (NotFoundHttpException $notFoundHttpException) {
  225.             $this->handlePage(Response::HTTP_NOT_FOUND);
  226.             return $this->display();
  227.         } catch (InsufficientAuthenticationException $insufficientAuthenticationException) {
  228.             $this->redirectToLogin();
  229.         }
  230.     }
  231.     /**
  232.      * Redirects to the login page in a way that the login page will redirect back to the current page after logging in
  233.      */
  234.     private function redirectToLogin()
  235.     {
  236.         $this->redirect(
  237.             Navigation::getUrlForBlock('Profiles''Login') . '?queryString=' Model::getRequest()->getRequestUri(),
  238.             Response::HTTP_TEMPORARY_REDIRECT
  239.         );
  240.     }
  241.     public function getExtras(): array
  242.     {
  243.         return $this->extras;
  244.     }
  245.     public function getId(): int
  246.     {
  247.         return $this->pageId;
  248.     }
  249.     private function getPageRecord(int $pageId): array
  250.     {
  251.         if ($this->url->getParameter('page_revision''int') === null) {
  252.             return Model::getPage($pageId);
  253.         }
  254.         // add no-index to meta-custom, so the draft won't get accidentally indexed
  255.         $this->header->addMetaData(['name' => 'robots''content' => 'noindex, nofollow'], true);
  256.         return Model::getPageRevision($this->url->getParameter('page_revision''int'));
  257.     }
  258.     protected function getPageContent(int $pageId): array
  259.     {
  260.         $record $this->getPageRecord($pageId);
  261.         if (empty($record)) {
  262.             return [];
  263.         }
  264.         // redirect to the first child if the page is empty
  265.         if ($this->allPositionsAreEmpty($record['positions'])) {
  266.             $firstChildId Navigation::getFirstChildId($record['id']);
  267.             // check if we actually have a first child
  268.             if ($firstChildId !== 0) {
  269.                 $this->redirect(Navigation::getUrl($firstChildId));
  270.             }
  271.         }
  272.         return $record;
  273.     }
  274.     private function allPositionsAreEmpty(array $positions): bool
  275.     {
  276.         // loop positions to check if they are empty
  277.         foreach ($positions as $blocks) {
  278.             // loop blocks in position
  279.             foreach ($blocks as $block) {
  280.                 // It isn't empty if HTML is provided, a decent extra is provided or a widget is provided
  281.                 if ($block['extra_type'] === 'block'
  282.                     || $block['extra_type'] === 'widget'
  283.                     || trim($block['html']) !== ''
  284.                 ) {
  285.                     return false;
  286.                 }
  287.             }
  288.         }
  289.         return true;
  290.     }
  291.     public function getRecord(): array
  292.     {
  293.         return $this->record;
  294.     }
  295.     public function getStatusCode(): int
  296.     {
  297.         return $this->statusCode;
  298.     }
  299.     protected function parseLanguages(): void
  300.     {
  301.         // just execute if the site is multi-language
  302.         if (!$this->getContainer()->getParameter('site.multilanguage') || count(Language::getActiveLanguages()) === 1) {
  303.             return;
  304.         }
  305.         $this->template->assignGlobal(
  306.             'languages',
  307.             array_map(
  308.                 function (string $language) {
  309.                     return [
  310.                         'url' => '/' $language,
  311.                         'label' => $language,
  312.                         'name' => Language::msg(mb_strtoupper($language)),
  313.                         'current' => $language === LANGUAGE,
  314.                     ];
  315.                 },
  316.                 Language::getActiveLanguages()
  317.             )
  318.         );
  319.     }
  320.     protected function parsePositions(): void
  321.     {
  322.         // init array to store parsed positions data
  323.         $positions = [];
  324.         // fetch variables from main template
  325.         $mainVariables $this->template->getAssignedVariables();
  326.         // loop all positions
  327.         foreach ($this->record['positions'] as $position => $blocks) {
  328.             // loop all blocks in this position
  329.             foreach ($blocks as $i => $block) {
  330.                 $positions[$position][$i] = $this->parseBlock($block$mainVariables);
  331.             }
  332.             // assign position to template
  333.             $this->template->assign('position' . \SpoonFilter::ucfirst($position), $positions[$position]);
  334.         }
  335.         $this->template->assign('positions'$positions);
  336.     }
  337.     private function parseBlock(array $block, array $mainVariables): array
  338.     {
  339.         if (!isset($block['extra'])) {
  340.             $parsedBlock $block;
  341.             if (array_key_exists('blockContent'$block)) {
  342.                 $parsedBlock['html'] = $block['blockContent'];
  343.             }
  344.             return $parsedBlock;
  345.         }
  346.         $block['extra']->execute();
  347.         $extraVariables $block['extra']->getTemplate()->getAssignedVariables();
  348.         $block['extra']->getTemplate()->assignArray($mainVariables);
  349.         $block['extra']->getTemplate()->assignArray($extraVariables);
  350.         return [
  351.             'variables' => $block['extra']->getTemplate()->getAssignedVariables(),
  352.             'blockIsEditor' => false,
  353.             'html' => $block['extra']->getContent(),
  354.         ];
  355.     }
  356.     protected function processExtra(ModuleExtraInterface $extra): void
  357.     {
  358.         $this->getContainer()->get('logger.public')->info(
  359.             'Executing ' get_class($extra) . " '{$extra->getAction()}' for module '{$extra->getModule()}'."
  360.         );
  361.         // overwrite the template
  362.         if (is_callable([$extra'getOverwrite']) && $extra->getOverwrite()) {
  363.             $this->templatePath $extra->getTemplatePath();
  364.         }
  365.     }
  366.     private function addAlternateLinks(): void
  367.     {
  368.         // no need for alternate links if there is only one language
  369.         if (!$this->getContainer()->getParameter('site.multilanguage')) {
  370.             return;
  371.         }
  372.         array_map([$this'addAlternateLinkForLanguage'], Language::getActiveLanguages());
  373.     }
  374.     private function addAlternateLinkForLanguage(string $language): void
  375.     {
  376.         if ($language === LANGUAGE) {
  377.             return;
  378.         }
  379.         // Get page data
  380.         $pageInfo Model::getPage($this->pageId);
  381.         // Check if hreflang is set for language
  382.         if (isset($pageInfo['data']['hreflang_' $language])) {
  383.             $url Navigation::getUrl($pageInfo['data']['hreflang_' $language], $language);
  384.         } else {
  385.             $url Navigation::getUrl($this->pageId$language);
  386.         }
  387.         // remove last /
  388.         $url rtrim($url'/\\');
  389.         // Ignore 404 links
  390.         if ($this->pageId !== Response::HTTP_NOT_FOUND
  391.             && $url === Navigation::getUrl(Response::HTTP_NOT_FOUND$language)) {
  392.             return;
  393.         }
  394.         // Convert relative to absolute url
  395.         if (strpos($url'/') === 0) {
  396.             $url SITE_URL $url;
  397.         }
  398.         $this->header->addLink(['rel' => 'alternate''hreflang' => $language'href' => $url]);
  399.     }
  400.     private function assignPageMeta(): void
  401.     {
  402.         // set pageTitle
  403.         $this->header->setPageTitle(
  404.             $this->record['meta_title'],
  405.             $this->record['meta_title_overwrite']
  406.         );
  407.         // set meta-data
  408.         $this->header->addMetaDescription(
  409.             $this->record['meta_description'],
  410.             $this->record['meta_description_overwrite']
  411.         );
  412.         $this->header->addMetaKeywords(
  413.             $this->record['meta_keywords'],
  414.             $this->record['meta_keywords_overwrite']
  415.         );
  416.         $this->header->setMetaCustom($this->record['meta_custom']);
  417.         // advanced SEO-attributes
  418.         if (isset($this->record['meta_seo_index'])) {
  419.             $this->header->addMetaData(
  420.                 ['name' => 'robots''content' => $this->record['meta_seo_index']]
  421.             );
  422.         }
  423.         if (isset($this->record['meta_seo_follow'])) {
  424.             $this->header->addMetaData(
  425.                 ['name' => 'robots''content' => $this->record['meta_seo_follow']]
  426.             );
  427.         }
  428.     }
  429.     protected function processPage(): void
  430.     {
  431.         $this->assignPageMeta();
  432.         new Navigation($this->getKernel());
  433.         $this->addAlternateLinks();
  434.         // assign content
  435.         $pageInfo Navigation::getPageInfo($this->record['id']);
  436.         $this->record['has_children'] = $pageInfo['has_children'];
  437.         $this->template->assignGlobal('page'$this->record);
  438.         // set template path
  439.         $this->templatePath $this->record['template_path'];
  440.         // loop blocks
  441.         foreach ($this->record['positions'] as $position => &$blocks) {
  442.             // position not known in template = skip it
  443.             if (!in_array($position$this->record['template_data']['names'])) {
  444.                 continue;
  445.             }
  446.             $blocks array_map(
  447.                 function (array $block) {
  448.                     if ($block['extra_id'] === null) {
  449.                         return [
  450.                             'blockIsEditor' => true,
  451.                             'blockContent' => $block['html'],
  452.                         ];
  453.                     }
  454.                     $block = ['extra' => $this->getExtraForBlock($block)];
  455.                     // add to list of extras to parse
  456.                     $this->extras[] = $block['extra'];
  457.                     return $block;
  458.                 },
  459.                 $blocks
  460.             );
  461.         }
  462.     }
  463.     private function getExtraForBlock(array $block): ModuleExtraInterface
  464.     {
  465.         // block
  466.         if ($block['extra_type'] === 'block') {
  467.             if (extension_loaded('newrelic')) {
  468.                 newrelic_name_transaction($block['extra_module'] . '::' $block['extra_action']);
  469.             }
  470.             return new FrontendBlockExtra(
  471.                 $this->getKernel(),
  472.                 $block['extra_module'],
  473.                 $block['extra_action'],
  474.                 $block['extra_data']
  475.             );
  476.         }
  477.         return new FrontendBlockWidget(
  478.             $this->getKernel(),
  479.             $block['extra_module'],
  480.             $block['extra_action'],
  481.             $block['extra_data']
  482.         );
  483.     }
  484.     private function redirect(string $urlint $code RedirectResponse::HTTP_FOUND): void
  485.     {
  486.         throw new RedirectException('Redirect', new RedirectResponse($url$code));
  487.     }
  488.     public static function getCurrentMainBlock(): ?FrontendBlockExtra
  489.     {
  490.         $extras FrontendModel::getContainer()->get('page')->getExtras();
  491.         foreach ($extras as $extra) {
  492.             if ($extra instanceof FrontendBlockExtra && !$extra instanceof FrontendBaseWidget) {
  493.                 return $extra;
  494.             }
  495.         }
  496.         return null;
  497.     }
  498. }