vendor/contao/core-bundle/src/Resources/contao/modules/Module.php line 209

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. use Contao\CoreBundle\Security\ContaoCorePermissions;
  11. use Contao\Model\Collection;
  12. use Symfony\Component\Routing\Exception\ExceptionInterface;
  13. /**
  14.  * Parent class for front end modules.
  15.  *
  16.  * @property integer $id
  17.  * @property integer $pid
  18.  * @property integer $tstamp
  19.  * @property string  $name
  20.  * @property string  $headline
  21.  * @property string  $type
  22.  * @property integer $levelOffset
  23.  * @property integer $showLevel
  24.  * @property boolean $hardLimit
  25.  * @property boolean $showProtected
  26.  * @property boolean $defineRoot
  27.  * @property integer $rootPage
  28.  * @property string  $navigationTpl
  29.  * @property string  $customTpl
  30.  * @property array   $pages
  31.  * @property boolean $showHidden
  32.  * @property string  $customLabel
  33.  * @property boolean $autologin
  34.  * @property integer $jumpTo
  35.  * @property integer $overviewPage
  36.  * @property boolean $redirectBack
  37.  * @property string  $cols
  38.  * @property array   $editable
  39.  * @property string  $memberTpl
  40.  * @property integer $form
  41.  * @property string  $queryType
  42.  * @property boolean $fuzzy
  43.  * @property string  $contextLength
  44.  * @property integer $minKeywordLength
  45.  * @property integer $perPage
  46.  * @property string  $searchType
  47.  * @property string  $searchTpl
  48.  * @property string  $inColumn
  49.  * @property integer $skipFirst
  50.  * @property boolean $loadFirst
  51.  * @property string  $singleSRC
  52.  * @property string  $url
  53.  * @property string  $imgSize
  54.  * @property boolean $useCaption
  55.  * @property boolean $fullsize
  56.  * @property string  $multiSRC
  57.  * @property string  $orderSRC
  58.  * @property string  $html
  59.  * @property integer $rss_cache
  60.  * @property string  $rss_feed
  61.  * @property string  $rss_template
  62.  * @property integer $numberOfItems
  63.  * @property boolean $disableCaptcha
  64.  * @property string  $reg_groups
  65.  * @property boolean $reg_allowLogin
  66.  * @property boolean $reg_skipName
  67.  * @property string  $reg_close
  68.  * @property boolean $reg_deleteDir
  69.  * @property boolean $reg_assignDir
  70.  * @property string  $reg_homeDir
  71.  * @property boolean $reg_activate
  72.  * @property integer $reg_jumpTo
  73.  * @property string  $reg_text
  74.  * @property string  $reg_password
  75.  * @property boolean $protected
  76.  * @property string  $groups
  77.  * @property string  $cssID
  78.  * @property string  $hl
  79.  */
  80. abstract class Module extends Frontend
  81. {
  82.     /**
  83.      * Template
  84.      * @var string
  85.      */
  86.     protected $strTemplate;
  87.     /**
  88.      * Column
  89.      * @var string
  90.      */
  91.     protected $strColumn;
  92.     /**
  93.      * Model
  94.      * @var ModuleModel
  95.      */
  96.     protected $objModel;
  97.     /**
  98.      * Current record
  99.      * @var array
  100.      */
  101.     protected $arrData = array();
  102.     /**
  103.      * Style array
  104.      * @var array
  105.      */
  106.     protected $arrStyle = array();
  107.     /**
  108.      * Initialize the object
  109.      *
  110.      * @param ModuleModel $objModule
  111.      * @param string      $strColumn
  112.      */
  113.     public function __construct($objModule$strColumn='main')
  114.     {
  115.         if ($objModule instanceof Model || $objModule instanceof Collection)
  116.         {
  117.             /** @var ModuleModel $objModel */
  118.             $objModel $objModule;
  119.             if ($objModel instanceof Collection)
  120.             {
  121.                 $objModel $objModel->current();
  122.             }
  123.             $this->objModel $objModel;
  124.         }
  125.         parent::__construct();
  126.         $this->arrData $objModule->row();
  127.         $this->cssID StringUtil::deserialize($objModule->cssIDtrue);
  128.         if ($this->customTpl)
  129.         {
  130.             $request System::getContainer()->get('request_stack')->getCurrentRequest();
  131.             // Use the custom template unless it is a back end request
  132.             if (!$request || !System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request))
  133.             {
  134.                 $this->strTemplate $this->customTpl;
  135.             }
  136.         }
  137.         $arrHeadline StringUtil::deserialize($objModule->headline);
  138.         $this->headline = \is_array($arrHeadline) ? $arrHeadline['value'] ?? '' $arrHeadline;
  139.         $this->hl $arrHeadline['unit'] ?? 'h1';
  140.         $this->strColumn $strColumn;
  141.     }
  142.     /**
  143.      * Set an object property
  144.      *
  145.      * @param string $strKey
  146.      * @param mixed  $varValue
  147.      */
  148.     public function __set($strKey$varValue)
  149.     {
  150.         $this->arrData[$strKey] = $varValue;
  151.     }
  152.     /**
  153.      * Return an object property
  154.      *
  155.      * @param string $strKey
  156.      *
  157.      * @return mixed
  158.      */
  159.     public function __get($strKey)
  160.     {
  161.         return $this->arrData[$strKey] ?? parent::__get($strKey);
  162.     }
  163.     /**
  164.      * Check whether a property is set
  165.      *
  166.      * @param string $strKey
  167.      *
  168.      * @return boolean
  169.      */
  170.     public function __isset($strKey)
  171.     {
  172.         return isset($this->arrData[$strKey]);
  173.     }
  174.     /**
  175.      * Return the model
  176.      *
  177.      * @return Model
  178.      */
  179.     public function getModel()
  180.     {
  181.         return $this->objModel;
  182.     }
  183.     /**
  184.      * Parse the template
  185.      *
  186.      * @return string
  187.      */
  188.     public function generate()
  189.     {
  190.         $this->Template = new FrontendTemplate($this->strTemplate);
  191.         $this->Template->setData($this->arrData);
  192.         $this->compile();
  193.         // Do not change this order (see #6191)
  194.         $this->Template->style = !empty($this->arrStyle) ? implode(' '$this->arrStyle) : '';
  195.         $this->Template->class trim('mod_' $this->type ' ' . ($this->cssID[1] ?? ''));
  196.         $this->Template->cssID = !empty($this->cssID[0]) ? ' id="' $this->cssID[0] . '"' '';
  197.         $this->Template->inColumn $this->strColumn;
  198.         if (!$this->Template->headline)
  199.         {
  200.             $this->Template->headline $this->headline;
  201.         }
  202.         if (!$this->Template->hl)
  203.         {
  204.             $this->Template->hl $this->hl;
  205.         }
  206.         if (!empty($this->objModel->classes) && \is_array($this->objModel->classes))
  207.         {
  208.             $this->Template->class .= ' ' implode(' '$this->objModel->classes);
  209.         }
  210.         // Tag the module (see #2137)
  211.         if (System::getContainer()->has('fos_http_cache.http.symfony_response_tagger') && !empty($tags $this->getResponseCacheTags()))
  212.         {
  213.             $responseTagger System::getContainer()->get('fos_http_cache.http.symfony_response_tagger');
  214.             $responseTagger->addTags($tags);
  215.         }
  216.         return $this->Template->parse();
  217.     }
  218.     /**
  219.      * Compile the current element
  220.      */
  221.     abstract protected function compile();
  222.     /**
  223.      * Get a list of tags that should be applied to the response when calling generate().
  224.      */
  225.     protected function getResponseCacheTags(): array
  226.     {
  227.         if ($this->objModel === null)
  228.         {
  229.             return array();
  230.         }
  231.         return array(System::getContainer()->get('contao.cache.entity_tags')->getTagForModelInstance($this->objModel));
  232.     }
  233.     /**
  234.      * Recursively compile the navigation menu and return it as HTML string
  235.      *
  236.      * @param integer $pid
  237.      * @param integer $level
  238.      * @param string  $host
  239.      * @param string  $language
  240.      *
  241.      * @return string
  242.      */
  243.     protected function renderNavigation($pid$level=1$host=null$language=null)
  244.     {
  245.         // Get all active subpages
  246.         $arrSubpages = static::getPublishedSubpagesByPid($pid$this->showHidden$this instanceof ModuleSitemap);
  247.         if ($arrSubpages === null)
  248.         {
  249.             return '';
  250.         }
  251.         $items = array();
  252.         $security System::getContainer()->get('security.helper');
  253.         $isMember $security->isGranted('ROLE_MEMBER');
  254.         $blnShowUnpublished System::getContainer()->get('contao.security.token_checker')->isPreviewMode();
  255.         $objTemplate = new FrontendTemplate($this->navigationTpl ?: 'nav_default');
  256.         $objTemplate->pid $pid;
  257.         $objTemplate->type = static::class;
  258.         $objTemplate->cssID $this->cssID// see #4897
  259.         $objTemplate->level 'level_' $level++;
  260.         $objTemplate->module $this// see #155
  261.         /** @var PageModel $objPage */
  262.         global $objPage;
  263.         // Browse subpages
  264.         foreach ($arrSubpages as list('page' => $objSubpage'hasSubpages' => $blnHasSubpages))
  265.         {
  266.             // Skip hidden sitemap pages
  267.             if ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_never')
  268.             {
  269.                 continue;
  270.             }
  271.             $objSubpage->loadDetails();
  272.             // Override the domain (see #3765)
  273.             if ($host !== null)
  274.             {
  275.                 $objSubpage->domain $host;
  276.             }
  277.             if ($objSubpage->tabindex 0)
  278.             {
  279.                 trigger_deprecation('contao/core-bundle''4.12''Using a tabindex value greater than 0 has been deprecated and will no longer work in Contao 5.0.');
  280.             }
  281.             // Hide the page if it is not protected and only visible to guests (backwards compatibility)
  282.             if ($objSubpage->guests && !$objSubpage->protected && $isMember)
  283.             {
  284.                 trigger_deprecation('contao/core-bundle''4.12''Using the "show to guests only" feature has been deprecated an will no longer work in Contao 5.0. Use the "protect page" function instead.');
  285.                 continue;
  286.             }
  287.             $subitems '';
  288.             // PageModel->groups is an array after calling loadDetails()
  289.             if (!$objSubpage->protected || $this->showProtected || ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_always') || $security->isGranted(ContaoCorePermissions::MEMBER_IN_GROUPS$objSubpage->groups))
  290.             {
  291.                 // Check whether there will be subpages
  292.                 if ($blnHasSubpages && (!$this->showLevel || $this->showLevel >= $level || (!$this->hardLimit && ($objPage->id == $objSubpage->id || \in_array($objPage->id$this->Database->getChildRecords($objSubpage->id'tl_page'))))))
  293.                 {
  294.                     $subitems $this->renderNavigation($objSubpage->id$level$host$language);
  295.                 }
  296.                 // Get href
  297.                 switch ($objSubpage->type)
  298.                 {
  299.                     case 'redirect':
  300.                         $href $objSubpage->url;
  301.                         if (strncasecmp($href'mailto:'7) === 0)
  302.                         {
  303.                             $href StringUtil::encodeEmail($href);
  304.                         }
  305.                         break;
  306.                     case 'forward':
  307.                         if ($objSubpage->jumpTo)
  308.                         {
  309.                             $objNext PageModel::findPublishedById($objSubpage->jumpTo);
  310.                         }
  311.                         else
  312.                         {
  313.                             $objNext PageModel::findFirstPublishedRegularByPid($objSubpage->id);
  314.                         }
  315.                         // Hide the link if the target page is invisible
  316.                         if (!$objNext instanceof PageModel || (!$objNext->loadDetails()->isPublic && !$blnShowUnpublished))
  317.                         {
  318.                             continue 2;
  319.                         }
  320.                         try
  321.                         {
  322.                             $href $objNext->getFrontendUrl();
  323.                         }
  324.                         catch (ExceptionInterface $exception)
  325.                         {
  326.                             continue 2;
  327.                         }
  328.                         break;
  329.                     default:
  330.                         try
  331.                         {
  332.                             $href $objSubpage->getFrontendUrl();
  333.                         }
  334.                         catch (ExceptionInterface $exception)
  335.                         {
  336.                             continue 2;
  337.                         }
  338.                         break;
  339.                 }
  340.                 $items[] = $this->compileNavigationRow($objPage$objSubpage$subitems$href);
  341.             }
  342.         }
  343.         // Add classes first and last
  344.         if (!empty($items))
  345.         {
  346.             $last = \count($items) - 1;
  347.             $items[0]['class'] = trim($items[0]['class'] . ' first');
  348.             $items[$last]['class'] = trim($items[$last]['class'] . ' last');
  349.         }
  350.         $objTemplate->items $items;
  351.         return !empty($items) ? $objTemplate->parse() : '';
  352.     }
  353.     /**
  354.      * Compile the navigation row and return it as array
  355.      *
  356.      * @param PageModel $objPage
  357.      * @param PageModel $objSubpage
  358.      * @param string    $subitems
  359.      * @param string    $href
  360.      *
  361.      * @return array
  362.      */
  363.     protected function compileNavigationRow(PageModel $objPagePageModel $objSubpage$subitems$href)
  364.     {
  365.         $row $objSubpage->row();
  366.         $trail = \in_array($objSubpage->id$objPage->trail);
  367.         // Use the path without query string to check for active pages (see #480)
  368.         list($path) = explode('?'Environment::get('request'), 2);
  369.         // Active page
  370.         if (($objPage->id == $objSubpage->id || ($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo)) && !($this instanceof ModuleSitemap) && $href == $path)
  371.         {
  372.             // Mark active forward pages (see #4822)
  373.             $strClass = (($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo) ? 'forward' . ($trail ' trail' '') : 'active') . ($subitems ' submenu' '') . ($objSubpage->protected ' protected' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  374.             $row['isActive'] = true;
  375.             $row['isTrail'] = false;
  376.         }
  377.         // Regular page
  378.         else
  379.         {
  380.             $strClass = ($subitems 'submenu' '') . ($objSubpage->protected ' protected' '') . ($trail ' trail' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  381.             // Mark pages on the same level (see #2419)
  382.             if ($objSubpage->pid == $objPage->pid)
  383.             {
  384.                 $strClass .= ' sibling';
  385.             }
  386.             $row['isActive'] = false;
  387.             $row['isTrail'] = $trail;
  388.         }
  389.         $row['subitems'] = $subitems;
  390.         $row['class'] = trim($strClass);
  391.         $row['title'] = StringUtil::specialchars($objSubpage->titletrue);
  392.         $row['pageTitle'] = StringUtil::specialchars($objSubpage->pageTitletrue);
  393.         $row['link'] = $objSubpage->title;
  394.         $row['href'] = $href;
  395.         $row['rel'] = '';
  396.         $row['nofollow'] = false// backwards compatibility
  397.         $row['target'] = '';
  398.         $row['description'] = str_replace(array("\n""\r"), array(' '''), $objSubpage->description);
  399.         $arrRel = array();
  400.         // Override the link target
  401.         if ($objSubpage->type == 'redirect' && $objSubpage->target)
  402.         {
  403.             $arrRel[] = 'noreferrer';
  404.             $arrRel[] = 'noopener';
  405.             $row['target'] = ' target="_blank"';
  406.         }
  407.         // Set the rel attribute
  408.         if (!empty($arrRel))
  409.         {
  410.             $row['rel'] = ' rel="' implode(' '$arrRel) . '"';
  411.         }
  412.         return $row;
  413.     }
  414.     /**
  415.      * Get all published pages by their parent ID and add the "hasSubpages" property
  416.      *
  417.      * @param integer $intPid        The parent page's ID
  418.      * @param boolean $blnShowHidden If true, hidden pages will be included
  419.      * @param boolean $blnIsSitemap  If true, the sitemap settings apply
  420.      *
  421.      * @return array<array{page:PageModel,hasSubpages:bool}>|null
  422.      */
  423.     protected static function getPublishedSubpagesByPid($intPid$blnShowHidden=false$blnIsSitemap=false): ?array
  424.     {
  425.         $time Date::floorToMinute();
  426.         $tokenChecker System::getContainer()->get('contao.security.token_checker');
  427.         $blnBeUserLoggedIn $tokenChecker->isPreviewMode();
  428.         $unroutableTypes System::getContainer()->get('contao.routing.page_registry')->getUnroutableTypes();
  429.         $arrPages Database::getInstance()->prepare("SELECT p1.id, EXISTS(SELECT * FROM tl_page p2 WHERE p2.pid=p1.id AND p2.type!='root' AND p2.type NOT IN ('" implode("', '"$unroutableTypes) . "')" . (!$blnShowHidden ? ($blnIsSitemap " AND (p2.hide='' OR sitemap='map_always')" " AND p2.hide=''") : "") . (!$blnBeUserLoggedIn " AND p2.published='1' AND (p2.start='' OR p2.start<='$time') AND (p2.stop='' OR p2.stop>'$time')" "") . ") AS hasSubpages FROM tl_page p1 WHERE p1.pid=? AND p1.type!='root' AND p1.type NOT IN ('" implode("', '"$unroutableTypes) . "')" . (!$blnShowHidden ? ($blnIsSitemap " AND (p1.hide='' OR sitemap='map_always')" " AND p1.hide=''") : "") . (!$blnBeUserLoggedIn " AND p1.published='1' AND (p1.start='' OR p1.start<='$time') AND (p1.stop='' OR p1.stop>'$time')" "") . " ORDER BY p1.sorting")
  430.                                            ->execute($intPid)
  431.                                            ->fetchAllAssoc();
  432.         if (\count($arrPages) < 1)
  433.         {
  434.             return null;
  435.         }
  436.         // Load models into the registry with a single query
  437.         PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  438.         return array_map(
  439.             static function (array $row): array
  440.             {
  441.                 return array(
  442.                     'page' => PageModel::findByPk($row['id']),
  443.                     'hasSubpages' => (bool) $row['hasSubpages'],
  444.                 );
  445.             },
  446.             $arrPages
  447.         );
  448.     }
  449.     /**
  450.      * Get all published pages by their parent ID and exclude pages only visible for guests
  451.      *
  452.      * @param integer $intPid        The parent page's ID
  453.      * @param boolean $blnShowHidden If true, hidden pages will be included
  454.      * @param boolean $blnIsSitemap  If true, the sitemap settings apply
  455.      *
  456.      * @return array<array{page:PageModel,hasSubpages:bool}>|null
  457.      *
  458.      * @deprecated Deprecated since Contao 4.12, to be removed in Contao 5.0;
  459.      *             use Module::getPublishedSubpagesByPid() instead and filter the guests pages yourself.
  460.      */
  461.     protected static function getPublishedSubpagesWithoutGuestsByPid($intPid$blnShowHidden=false$blnIsSitemap=false): ?array
  462.     {
  463.         trigger_deprecation('contao/core-bundle''4.9''Using Module::getPublishedSubpagesWithoutGuestsByPid() has been deprecated and will no longer work Contao 5.0. Use Module::getPublishedSubpagesByPid() instead and filter the guests pages yourself.');
  464.         $time Date::floorToMinute();
  465.         $tokenChecker System::getContainer()->get('contao.security.token_checker');
  466.         $blnFeUserLoggedIn $tokenChecker->hasFrontendUser();
  467.         $blnBeUserLoggedIn $tokenChecker->isPreviewMode();
  468.         $unroutableTypes System::getContainer()->get('contao.routing.page_registry')->getUnroutableTypes();
  469.         $arrPages Database::getInstance()->prepare("SELECT p1.id, EXISTS(SELECT * FROM tl_page p2 WHERE p2.pid=p1.id AND p2.type!='root' AND p2.type NOT IN ('" implode("', '"$unroutableTypes) . "')" . (!$blnShowHidden ? ($blnIsSitemap " AND (p2.hide='' OR sitemap='map_always')" " AND p2.hide=''") : "") . ($blnFeUserLoggedIn " AND p2.guests=''" "") . (!$blnBeUserLoggedIn " AND p2.published='1' AND (p2.start='' OR p2.start<='$time') AND (p2.stop='' OR p2.stop>'$time')" "") . ") AS hasSubpages FROM tl_page p1 WHERE p1.pid=? AND p1.type!='root' AND p1.type NOT IN ('" implode("', '"$unroutableTypes) . "')" . (!$blnShowHidden ? ($blnIsSitemap " AND (p1.hide='' OR sitemap='map_always')" " AND p1.hide=''") : "") . ($blnFeUserLoggedIn " AND p1.guests=''" "") . (!$blnBeUserLoggedIn " AND p1.published='1' AND (p1.start='' OR p1.start<='$time') AND (p1.stop='' OR p1.stop>'$time')" "") . " ORDER BY p1.sorting")
  470.                                            ->execute($intPid)
  471.                                            ->fetchAllAssoc();
  472.         if (\count($arrPages) < 1)
  473.         {
  474.             return null;
  475.         }
  476.         // Load models into the registry with a single query
  477.         PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  478.         return array_map(
  479.             static function (array $row): array
  480.             {
  481.                 return array(
  482.                     'page' => PageModel::findByPk($row['id']),
  483.                     'hasSubpages' => (bool) $row['hasSubpages'],
  484.                 );
  485.             },
  486.             $arrPages
  487.         );
  488.     }
  489.     /**
  490.      * Find a front end module in the FE_MOD array and return the class name
  491.      *
  492.      * @param string $strName The front end module name
  493.      *
  494.      * @return string The class name
  495.      */
  496.     public static function findClass($strName)
  497.     {
  498.         foreach ($GLOBALS['FE_MOD'] as $v)
  499.         {
  500.             foreach ($v as $kk=>$vv)
  501.             {
  502.                 if ($kk == $strName)
  503.                 {
  504.                     return $vv;
  505.                 }
  506.             }
  507.         }
  508.         return '';
  509.     }
  510. }
  511. class_alias(Module::class, 'Module');