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

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\Model\Collection;
  11. /**
  12.  * Parent class for front end modules.
  13.  *
  14.  * @property integer $id
  15.  * @property integer $pid
  16.  * @property integer $tstamp
  17.  * @property string  $name
  18.  * @property string  $headline
  19.  * @property string  $type
  20.  * @property integer $levelOffset
  21.  * @property integer $showLevel
  22.  * @property boolean $hardLimit
  23.  * @property boolean $showProtected
  24.  * @property boolean $defineRoot
  25.  * @property integer $rootPage
  26.  * @property string  $navigationTpl
  27.  * @property string  $customTpl
  28.  * @property array   $pages
  29.  * @property string  $orderPages
  30.  * @property boolean $showHidden
  31.  * @property string  $customLabel
  32.  * @property boolean $autologin
  33.  * @property integer $jumpTo
  34.  * @property boolean $redirectBack
  35.  * @property string  $cols
  36.  * @property array   $editable
  37.  * @property string  $memberTpl
  38.  * @property integer $form
  39.  * @property string  $queryType
  40.  * @property boolean $fuzzy
  41.  * @property string  $contextLength
  42.  * @property integer $minKeywordLength
  43.  * @property integer $perPage
  44.  * @property string  $searchType
  45.  * @property string  $searchTpl
  46.  * @property string  $inColumn
  47.  * @property integer $skipFirst
  48.  * @property boolean $loadFirst
  49.  * @property string  $singleSRC
  50.  * @property string  $url
  51.  * @property string  $imgSize
  52.  * @property boolean $useCaption
  53.  * @property boolean $fullsize
  54.  * @property string  $multiSRC
  55.  * @property string  $orderSRC
  56.  * @property string  $html
  57.  * @property integer $rss_cache
  58.  * @property string  $rss_feed
  59.  * @property string  $rss_template
  60.  * @property integer $numberOfItems
  61.  * @property boolean $disableCaptcha
  62.  * @property string  $reg_groups
  63.  * @property boolean $reg_allowLogin
  64.  * @property boolean $reg_skipName
  65.  * @property string  $reg_close
  66.  * @property boolean $reg_assignDir
  67.  * @property string  $reg_homeDir
  68.  * @property boolean $reg_activate
  69.  * @property integer $reg_jumpTo
  70.  * @property string  $reg_text
  71.  * @property string  $reg_password
  72.  * @property boolean $protected
  73.  * @property string  $groups
  74.  * @property boolean $guests
  75.  * @property string  $cssID
  76.  * @property string  $hl
  77.  *
  78.  * @author Leo Feyer <https://github.com/leofeyer>
  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.         return array('contao.db.tl_module.' $this->id);
  228.     }
  229.     /**
  230.      * Recursively compile the navigation menu and return it as HTML string
  231.      *
  232.      * @param integer $pid
  233.      * @param integer $level
  234.      * @param string  $host
  235.      * @param string  $language
  236.      *
  237.      * @return string
  238.      */
  239.     protected function renderNavigation($pid$level=1$host=null$language=null)
  240.     {
  241.         // Get all active subpages
  242.         $arrSubpages = static::getPublishedSubpagesWithoutGuestsByPid($pid$this->showHidden$this instanceof ModuleSitemap);
  243.         if ($arrSubpages === null)
  244.         {
  245.             return '';
  246.         }
  247.         $items = array();
  248.         $groups = array();
  249.         // Get all groups of the current front end user
  250.         if (System::getContainer()->get('contao.security.token_checker')->hasFrontendUser())
  251.         {
  252.             $this->import(FrontendUser::class, 'User');
  253.             $groups $this->User->groups;
  254.         }
  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.             $subitems '';
  272.             $_groups StringUtil::deserialize($objSubpage->groups);
  273.             // Override the domain (see #3765)
  274.             if ($host !== null)
  275.             {
  276.                 $objSubpage->domain $host;
  277.             }
  278.             // Do not show protected pages unless a front end user is logged in
  279.             if (!$objSubpage->protected || $this->showProtected || ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_always') || (\is_array($_groups) && \is_array($groups) && \count(array_intersect($_groups$groups))))
  280.             {
  281.                 // Check whether there will be subpages
  282.                 if ($blnHasSubpages && (!$this->showLevel || $this->showLevel >= $level || (!$this->hardLimit && ($objPage->id == $objSubpage->id || \in_array($objPage->id$this->Database->getChildRecords($objSubpage->id'tl_page'))))))
  283.                 {
  284.                     $subitems $this->renderNavigation($objSubpage->id$level$host$language);
  285.                 }
  286.                 // Get href
  287.                 switch ($objSubpage->type)
  288.                 {
  289.                     case 'redirect':
  290.                         $href $objSubpage->url;
  291.                         if (strncasecmp($href'mailto:'7) === 0)
  292.                         {
  293.                             $href StringUtil::encodeEmail($href);
  294.                         }
  295.                         break;
  296.                     case 'forward':
  297.                         if ($objSubpage->jumpTo)
  298.                         {
  299.                             $objNext PageModel::findPublishedById($objSubpage->jumpTo);
  300.                         }
  301.                         else
  302.                         {
  303.                             $objNext PageModel::findFirstPublishedRegularByPid($objSubpage->id);
  304.                         }
  305.                         // Hide the link if the target page is invisible
  306.                         if (!$objNext instanceof PageModel || (!$objNext->loadDetails()->isPublic && !BE_USER_LOGGED_IN))
  307.                         {
  308.                             continue 2;
  309.                         }
  310.                         $href $objNext->getFrontendUrl();
  311.                         break;
  312.                     default:
  313.                         $href $objSubpage->getFrontendUrl();
  314.                         break;
  315.                 }
  316.                 $items[] = $this->compileNavigationRow($objPage$objSubpage$subitems$href);
  317.             }
  318.         }
  319.         // Add classes first and last
  320.         if (!empty($items))
  321.         {
  322.             $last = \count($items) - 1;
  323.             $items[0]['class'] = trim($items[0]['class'] . ' first');
  324.             $items[$last]['class'] = trim($items[$last]['class'] . ' last');
  325.         }
  326.         $objTemplate->items $items;
  327.         return !empty($items) ? $objTemplate->parse() : '';
  328.     }
  329.     /**
  330.      * Compile the navigation row and return it as array
  331.      *
  332.      * @param PageModel $objPage
  333.      * @param PageModel $objSubpage
  334.      * @param string    $subitems
  335.      * @param string    $href
  336.      *
  337.      * @return array
  338.      */
  339.     protected function compileNavigationRow(PageModel $objPagePageModel $objSubpage$subitems$href)
  340.     {
  341.         $row $objSubpage->row();
  342.         $trail = \in_array($objSubpage->id$objPage->trail);
  343.         // Use the path without query string to check for active pages (see #480)
  344.         list($path) = explode('?'Environment::get('request'), 2);
  345.         // Active page
  346.         if (($objPage->id == $objSubpage->id || ($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo)) && !($this instanceof ModuleSitemap) && $href == $path)
  347.         {
  348.             // Mark active forward pages (see #4822)
  349.             $strClass = (($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo) ? 'forward' . ($trail ' trail' '') : 'active') . ($subitems ' submenu' '') . ($objSubpage->protected ' protected' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  350.             $row['isActive'] = true;
  351.             $row['isTrail'] = false;
  352.         }
  353.         // Regular page
  354.         else
  355.         {
  356.             $strClass = ($subitems 'submenu' '') . ($objSubpage->protected ' protected' '') . ($trail ' trail' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  357.             // Mark pages on the same level (see #2419)
  358.             if ($objSubpage->pid == $objPage->pid)
  359.             {
  360.                 $strClass .= ' sibling';
  361.             }
  362.             $row['isActive'] = false;
  363.             $row['isTrail'] = $trail;
  364.         }
  365.         $row['subitems'] = $subitems;
  366.         $row['class'] = trim($strClass);
  367.         $row['title'] = StringUtil::specialchars($objSubpage->titletrue);
  368.         $row['pageTitle'] = StringUtil::specialchars($objSubpage->pageTitletrue);
  369.         $row['link'] = $objSubpage->title;
  370.         $row['href'] = $href;
  371.         $row['rel'] = '';
  372.         $row['nofollow'] = false// backwards compatibility
  373.         $row['target'] = '';
  374.         $row['description'] = str_replace(array("\n""\r"), array(' '''), $objSubpage->description);
  375.         $arrRel = array();
  376.         // Override the link target
  377.         if ($objSubpage->type == 'redirect' && $objSubpage->target)
  378.         {
  379.             $arrRel[] = 'noreferrer';
  380.             $arrRel[] = 'noopener';
  381.             $row['target'] = ' target="_blank"';
  382.         }
  383.         // Set the rel attribute
  384.         if (!empty($arrRel))
  385.         {
  386.             $row['rel'] = ' rel="' implode(' '$arrRel) . '"';
  387.         }
  388.         return $row;
  389.     }
  390.     /**
  391.      * Get all published pages by their parent ID and exclude pages only visible for guests
  392.      *
  393.      * @param integer $intPid        The parent page's ID
  394.      * @param boolean $blnShowHidden If true, hidden pages will be included
  395.      * @param boolean $blnIsSitemap  If true, the sitemap settings apply
  396.      *
  397.      * @return array<array{page:PageModel,hasSubpages:bool}>|null
  398.      */
  399.     protected static function getPublishedSubpagesWithoutGuestsByPid($intPid$blnShowHidden=false$blnIsSitemap=false): ?array
  400.     {
  401.         $time Date::floorToMinute();
  402.         $tokenChecker System::getContainer()->get('contao.security.token_checker');
  403.         $blnFeUserLoggedIn $tokenChecker->hasFrontendUser();
  404.         $blnBeUserLoggedIn $tokenChecker->hasBackendUser() && $tokenChecker->isPreviewMode();
  405.         $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!='error_401' AND p2.type!='error_403' AND p2.type!='error_404'" . (!$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!='error_401' AND p1.type!='error_403' AND p1.type!='error_404'" . (!$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")
  406.                                            ->execute($intPid)
  407.                                            ->fetchAllAssoc();
  408.         if (\count($arrPages) < 1)
  409.         {
  410.             return null;
  411.         }
  412.         // Load models into the registry with a single query
  413.         PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  414.         return array_map(
  415.             static function (array $row): array
  416.             {
  417.                 return array(
  418.                     'page' => PageModel::findByPk($row['id']),
  419.                     'hasSubpages' => (bool) $row['hasSubpages'],
  420.                 );
  421.             },
  422.             $arrPages
  423.         );
  424.     }
  425.     /**
  426.      * Find a front end module in the FE_MOD array and return the class name
  427.      *
  428.      * @param string $strName The front end module name
  429.      *
  430.      * @return string The class name
  431.      */
  432.     public static function findClass($strName)
  433.     {
  434.         foreach ($GLOBALS['FE_MOD'] as $v)
  435.         {
  436.             foreach ($v as $kk=>$vv)
  437.             {
  438.                 if ($kk == $strName)
  439.                 {
  440.                     return $vv;
  441.                 }
  442.             }
  443.         }
  444.         return '';
  445.     }
  446. }
  447. class_alias(Module::class, 'Module');