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 boolean $redirectBack
  36.  * @property string  $cols
  37.  * @property array   $editable
  38.  * @property string  $memberTpl
  39.  * @property integer $form
  40.  * @property string  $queryType
  41.  * @property boolean $fuzzy
  42.  * @property string  $contextLength
  43.  * @property integer $minKeywordLength
  44.  * @property integer $perPage
  45.  * @property string  $searchType
  46.  * @property string  $searchTpl
  47.  * @property string  $inColumn
  48.  * @property integer $skipFirst
  49.  * @property boolean $loadFirst
  50.  * @property string  $singleSRC
  51.  * @property string  $url
  52.  * @property string  $imgSize
  53.  * @property boolean $useCaption
  54.  * @property boolean $fullsize
  55.  * @property string  $multiSRC
  56.  * @property string  $orderSRC
  57.  * @property string  $html
  58.  * @property integer $rss_cache
  59.  * @property string  $rss_feed
  60.  * @property string  $rss_template
  61.  * @property integer $numberOfItems
  62.  * @property boolean $disableCaptcha
  63.  * @property string  $reg_groups
  64.  * @property boolean $reg_allowLogin
  65.  * @property boolean $reg_skipName
  66.  * @property string  $reg_close
  67.  * @property boolean $reg_assignDir
  68.  * @property string  $reg_homeDir
  69.  * @property boolean $reg_activate
  70.  * @property integer $reg_jumpTo
  71.  * @property string  $reg_text
  72.  * @property string  $reg_password
  73.  * @property boolean $protected
  74.  * @property string  $groups
  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 = \is_array($arrHeadline) ? $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::getPublishedSubpagesByPid($pid$this->showHidden$this instanceof ModuleSitemap);
  243.         if ($arrSubpages === null)
  244.         {
  245.             return '';
  246.         }
  247.         $items = array();
  248.         $security System::getContainer()->get('security.helper');
  249.         $isMember $security->isGranted('ROLE_MEMBER');
  250.         $objTemplate = new FrontendTemplate($this->navigationTpl ?: 'nav_default');
  251.         $objTemplate->pid $pid;
  252.         $objTemplate->type = static::class;
  253.         $objTemplate->cssID $this->cssID// see #4897
  254.         $objTemplate->level 'level_' $level++;
  255.         $objTemplate->module $this// see #155
  256.         /** @var PageModel $objPage */
  257.         global $objPage;
  258.         // Browse subpages
  259.         foreach ($arrSubpages as list('page' => $objSubpage'hasSubpages' => $blnHasSubpages))
  260.         {
  261.             // Skip hidden sitemap pages
  262.             if ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_never')
  263.             {
  264.                 continue;
  265.             }
  266.             $objSubpage->loadDetails();
  267.             // Override the domain (see #3765)
  268.             if ($host !== null)
  269.             {
  270.                 $objSubpage->domain $host;
  271.             }
  272.             if ($objSubpage->tabindex 0)
  273.             {
  274.                 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.');
  275.             }
  276.             // Hide the page if it is not protected and only visible to guests (backwards compatibility)
  277.             if ($objSubpage->guests && !$objSubpage->protected && $isMember)
  278.             {
  279.                 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.');
  280.                 continue;
  281.             }
  282.             $subitems '';
  283.             // PageModel->groups is an array after calling loadDetails()
  284.             if (!$objSubpage->protected || $this->showProtected || ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_always') || $security->isGranted(ContaoCorePermissions::MEMBER_IN_GROUPS$objSubpage->groups))
  285.             {
  286.                 // Check whether there will be subpages
  287.                 if ($blnHasSubpages && (!$this->showLevel || $this->showLevel >= $level || (!$this->hardLimit && ($objPage->id == $objSubpage->id || \in_array($objPage->id$this->Database->getChildRecords($objSubpage->id'tl_page'))))))
  288.                 {
  289.                     $subitems $this->renderNavigation($objSubpage->id$level$host$language);
  290.                 }
  291.                 // Get href
  292.                 switch ($objSubpage->type)
  293.                 {
  294.                     case 'redirect':
  295.                         $href $objSubpage->url;
  296.                         if (strncasecmp($href'mailto:'7) === 0)
  297.                         {
  298.                             $href StringUtil::encodeEmail($href);
  299.                         }
  300.                         break;
  301.                     case 'forward':
  302.                         if ($objSubpage->jumpTo)
  303.                         {
  304.                             $objNext PageModel::findPublishedById($objSubpage->jumpTo);
  305.                         }
  306.                         else
  307.                         {
  308.                             $objNext PageModel::findFirstPublishedRegularByPid($objSubpage->id);
  309.                         }
  310.                         // Hide the link if the target page is invisible
  311.                         if (!$objNext instanceof PageModel || (!$objNext->loadDetails()->isPublic && !BE_USER_LOGGED_IN))
  312.                         {
  313.                             continue 2;
  314.                         }
  315.                         try
  316.                         {
  317.                             $href $objNext->getFrontendUrl();
  318.                         }
  319.                         catch (ExceptionInterface $exception)
  320.                         {
  321.                             System::log('Unable to generate URL for page ID ' $objSubpage->id ': ' $exception->getMessage(), __METHOD__TL_ERROR);
  322.                             continue 2;
  323.                         }
  324.                         break;
  325.                     default:
  326.                         try
  327.                         {
  328.                             $href $objSubpage->getFrontendUrl();
  329.                         }
  330.                         catch (ExceptionInterface $exception)
  331.                         {
  332.                             System::log('Unable to generate URL for page ID ' $objSubpage->id ': ' $exception->getMessage(), __METHOD__TL_ERROR);
  333.                             continue 2;
  334.                         }
  335.                         break;
  336.                 }
  337.                 $items[] = $this->compileNavigationRow($objPage$objSubpage$subitems$href);
  338.             }
  339.         }
  340.         // Add classes first and last
  341.         if (!empty($items))
  342.         {
  343.             $last = \count($items) - 1;
  344.             $items[0]['class'] = trim($items[0]['class'] . ' first');
  345.             $items[$last]['class'] = trim($items[$last]['class'] . ' last');
  346.         }
  347.         $objTemplate->items $items;
  348.         return !empty($items) ? $objTemplate->parse() : '';
  349.     }
  350.     /**
  351.      * Compile the navigation row and return it as array
  352.      *
  353.      * @param PageModel $objPage
  354.      * @param PageModel $objSubpage
  355.      * @param string    $subitems
  356.      * @param string    $href
  357.      *
  358.      * @return array
  359.      */
  360.     protected function compileNavigationRow(PageModel $objPagePageModel $objSubpage$subitems$href)
  361.     {
  362.         $row $objSubpage->row();
  363.         $trail = \in_array($objSubpage->id$objPage->trail);
  364.         // Use the path without query string to check for active pages (see #480)
  365.         list($path) = explode('?'Environment::get('request'), 2);
  366.         // Active page
  367.         if (($objPage->id == $objSubpage->id || ($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo)) && !($this instanceof ModuleSitemap) && $href == $path)
  368.         {
  369.             // Mark active forward pages (see #4822)
  370.             $strClass = (($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo) ? 'forward' . ($trail ' trail' '') : 'active') . ($subitems ' submenu' '') . ($objSubpage->protected ' protected' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  371.             $row['isActive'] = true;
  372.             $row['isTrail'] = false;
  373.         }
  374.         // Regular page
  375.         else
  376.         {
  377.             $strClass = ($subitems 'submenu' '') . ($objSubpage->protected ' protected' '') . ($trail ' trail' '') . ($objSubpage->cssClass ' ' $objSubpage->cssClass '');
  378.             // Mark pages on the same level (see #2419)
  379.             if ($objSubpage->pid == $objPage->pid)
  380.             {
  381.                 $strClass .= ' sibling';
  382.             }
  383.             $row['isActive'] = false;
  384.             $row['isTrail'] = $trail;
  385.         }
  386.         $row['subitems'] = $subitems;
  387.         $row['class'] = trim($strClass);
  388.         $row['title'] = StringUtil::specialchars($objSubpage->titletrue);
  389.         $row['pageTitle'] = StringUtil::specialchars($objSubpage->pageTitletrue);
  390.         $row['link'] = $objSubpage->title;
  391.         $row['href'] = $href;
  392.         $row['rel'] = '';
  393.         $row['nofollow'] = (strncmp($objSubpage->robots'noindex,nofollow'16) === 0); // backwards compatibility
  394.         $row['target'] = '';
  395.         $row['description'] = str_replace(array("\n""\r"), array(' '''), $objSubpage->description);
  396.         $arrRel = array();
  397.         if (strncmp($objSubpage->robots'noindex,nofollow'16) === 0)
  398.         {
  399.             $arrRel[] = 'nofollow';
  400.         }
  401.         // Override the link target
  402.         if ($objSubpage->type == 'redirect' && $objSubpage->target)
  403.         {
  404.             $arrRel[] = 'noreferrer';
  405.             $arrRel[] = 'noopener';
  406.             $row['target'] = ' target="_blank"';
  407.         }
  408.         // Set the rel attribute
  409.         if (!empty($arrRel))
  410.         {
  411.             $row['rel'] = ' rel="' implode(' '$arrRel) . '"';
  412.         }
  413.         return $row;
  414.     }
  415.     /**
  416.      * Get all published pages by their parent ID and add the "hasSubpages" property
  417.      *
  418.      * @param integer $intPid        The parent page's ID
  419.      * @param boolean $blnShowHidden If true, hidden pages will be included
  420.      * @param boolean $blnIsSitemap  If true, the sitemap settings apply
  421.      *
  422.      * @return array<array{page:PageModel,hasSubpages:bool}>|null
  423.      */
  424.     protected static function getPublishedSubpagesByPid($intPid$blnShowHidden=false$blnIsSitemap=false): ?array
  425.     {
  426.         $time Date::floorToMinute();
  427.         $tokenChecker System::getContainer()->get('contao.security.token_checker');
  428.         $blnBeUserLoggedIn $tokenChecker->hasBackendUser() && $tokenChecker->isPreviewMode();
  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!='error_401' AND p2.type!='error_403' AND p2.type!='error_404'" . (!$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!='error_401' AND p1.type!='error_403' AND p1.type!='error_404'" . (!$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_error('Using Module::getPublishedSubpagesWithoutGuestsByPid() has been deprecated and will no longer work Contao 5.0. Use Module::getPublishedSubpagesByPid() instead and filter the guests pages yourself.'E_USER_DEPRECATED);
  464.         $time Date::floorToMinute();
  465.         $tokenChecker System::getContainer()->get('contao.security.token_checker');
  466.         $blnFeUserLoggedIn $tokenChecker->hasFrontendUser();
  467.         $blnBeUserLoggedIn $tokenChecker->hasBackendUser() && $tokenChecker->isPreviewMode();
  468.         $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")
  469.                                            ->execute($intPid)
  470.                                            ->fetchAllAssoc();
  471.         if (\count($arrPages) < 1)
  472.         {
  473.             return null;
  474.         }
  475.         // Load models into the registry with a single query
  476.         PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  477.         return array_map(
  478.             static function (array $row): array
  479.             {
  480.                 return array(
  481.                     'page' => PageModel::findByPk($row['id']),
  482.                     'hasSubpages' => (bool) $row['hasSubpages'],
  483.                 );
  484.             },
  485.             $arrPages
  486.         );
  487.     }
  488.     /**
  489.      * Find a front end module in the FE_MOD array and return the class name
  490.      *
  491.      * @param string $strName The front end module name
  492.      *
  493.      * @return string The class name
  494.      */
  495.     public static function findClass($strName)
  496.     {
  497.         foreach ($GLOBALS['FE_MOD'] as $v)
  498.         {
  499.             foreach ($v as $kk=>$vv)
  500.             {
  501.                 if ($kk == $strName)
  502.                 {
  503.                     return $vv;
  504.                 }
  505.             }
  506.         }
  507.         return '';
  508.     }
  509. }
  510. class_alias(Module::class, 'Module');