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

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->cssID, true);
  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 $objPage, PageModel $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->title, true);
  392. $row['pageTitle'] = StringUtil::specialchars($objSubpage->pageTitle, true);
  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(' ', ''), (string) $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. // Tag the page
  413. if (System::getContainer()->has('fos_http_cache.http.symfony_response_tagger'))
  414. {
  415. $responseTagger = System::getContainer()->get('fos_http_cache.http.symfony_response_tagger');
  416. $responseTagger->addTags(array('contao.db.tl_page.' . $objSubpage->id));
  417. }
  418. return $row;
  419. }
  420. /**
  421. * Get all published pages by their parent ID and add the "hasSubpages" property
  422. *
  423. * @param integer $intPid The parent page's ID
  424. * @param boolean $blnShowHidden If true, hidden pages will be included
  425. * @param boolean $blnIsSitemap If true, the sitemap settings apply
  426. *
  427. * @return array<array{page:PageModel,hasSubpages:bool}>|null
  428. */
  429. protected static function getPublishedSubpagesByPid($intPid, $blnShowHidden=false, $blnIsSitemap=false): ?array
  430. {
  431. $time = Date::floorToMinute();
  432. $tokenChecker = System::getContainer()->get('contao.security.token_checker');
  433. $blnBeUserLoggedIn = $tokenChecker->isPreviewMode();
  434. $unroutableTypes = System::getContainer()->get('contao.routing.page_registry')->getUnroutableTypes();
  435. $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")
  436. ->execute($intPid)
  437. ->fetchAllAssoc();
  438. if (\count($arrPages) < 1)
  439. {
  440. return null;
  441. }
  442. // Load models into the registry with a single query
  443. PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  444. return array_map(
  445. static function (array $row): array
  446. {
  447. return array(
  448. 'page' => PageModel::findByPk($row['id']),
  449. 'hasSubpages' => (bool) $row['hasSubpages'],
  450. );
  451. },
  452. $arrPages
  453. );
  454. }
  455. /**
  456. * Get all published pages by their parent ID and exclude pages only visible for guests
  457. *
  458. * @param integer $intPid The parent page's ID
  459. * @param boolean $blnShowHidden If true, hidden pages will be included
  460. * @param boolean $blnIsSitemap If true, the sitemap settings apply
  461. *
  462. * @return array<array{page:PageModel,hasSubpages:bool}>|null
  463. *
  464. * @deprecated Deprecated since Contao 4.12, to be removed in Contao 5.0;
  465. * use Module::getPublishedSubpagesByPid() instead and filter the guests pages yourself.
  466. */
  467. protected static function getPublishedSubpagesWithoutGuestsByPid($intPid, $blnShowHidden=false, $blnIsSitemap=false): ?array
  468. {
  469. 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.');
  470. $time = Date::floorToMinute();
  471. $tokenChecker = System::getContainer()->get('contao.security.token_checker');
  472. $blnFeUserLoggedIn = $tokenChecker->hasFrontendUser();
  473. $blnBeUserLoggedIn = $tokenChecker->isPreviewMode();
  474. $unroutableTypes = System::getContainer()->get('contao.routing.page_registry')->getUnroutableTypes();
  475. $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")
  476. ->execute($intPid)
  477. ->fetchAllAssoc();
  478. if (\count($arrPages) < 1)
  479. {
  480. return null;
  481. }
  482. // Load models into the registry with a single query
  483. PageModel::findMultipleByIds(array_map(static function ($row) { return $row['id']; }, $arrPages));
  484. return array_map(
  485. static function (array $row): array
  486. {
  487. return array(
  488. 'page' => PageModel::findByPk($row['id']),
  489. 'hasSubpages' => (bool) $row['hasSubpages'],
  490. );
  491. },
  492. $arrPages
  493. );
  494. }
  495. /**
  496. * Find a front end module in the FE_MOD array and return the class name
  497. *
  498. * @param string $strName The front end module name
  499. *
  500. * @return string The class name
  501. */
  502. public static function findClass($strName)
  503. {
  504. foreach ($GLOBALS['FE_MOD'] as $v)
  505. {
  506. foreach ($v as $kk=>$vv)
  507. {
  508. if ($kk == $strName)
  509. {
  510. return $vv;
  511. }
  512. }
  513. }
  514. return '';
  515. }
  516. }
  517. class_alias(Module::class, 'Module');