1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Viewpage\Controller; 19 20use Psr\Http\Message\ResponseInterface; 21use Psr\Http\Message\ServerRequestInterface; 22use TYPO3\CMS\Backend\Routing\UriBuilder; 23use TYPO3\CMS\Backend\Template\Components\ButtonBar; 24use TYPO3\CMS\Backend\Template\ModuleTemplate; 25use TYPO3\CMS\Backend\Utility\BackendUtility; 26use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 27use TYPO3\CMS\Core\Context\LanguageAspectFactory; 28use TYPO3\CMS\Core\Domain\Repository\PageRepository; 29use TYPO3\CMS\Core\Exception\SiteNotFoundException; 30use TYPO3\CMS\Core\Http\HtmlResponse; 31use TYPO3\CMS\Core\Imaging\Icon; 32use TYPO3\CMS\Core\Imaging\IconFactory; 33use TYPO3\CMS\Core\Localization\LanguageService; 34use TYPO3\CMS\Core\Messaging\FlashMessage; 35use TYPO3\CMS\Core\Messaging\FlashMessageService; 36use TYPO3\CMS\Core\Page\PageRenderer; 37use TYPO3\CMS\Core\Routing\UnableToLinkToPageException; 38use TYPO3\CMS\Core\Site\SiteFinder; 39use TYPO3\CMS\Core\Utility\GeneralUtility; 40use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; 41use TYPO3\CMS\Fluid\View\StandaloneView; 42 43/** 44 * Controller for viewing the frontend 45 * @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API. 46 */ 47class ViewModuleController 48{ 49 /** 50 * ModuleTemplate object 51 * 52 * @var ModuleTemplate 53 */ 54 protected $moduleTemplate; 55 56 /** 57 * View 58 * 59 * @var ViewInterface 60 */ 61 protected $view; 62 63 /** 64 * Initialize module template and language service 65 */ 66 public function __construct() 67 { 68 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class); 69 $this->getLanguageService()->includeLLFile('EXT:viewpage/Resources/Private/Language/locallang.xlf'); 70 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); 71 $pageRenderer->addInlineLanguageLabelFile('EXT:viewpage/Resources/Private/Language/locallang.xlf'); 72 } 73 74 /** 75 * Initialize view 76 * 77 * @param string $templateName 78 */ 79 protected function initializeView(string $templateName) 80 { 81 $this->view = GeneralUtility::makeInstance(StandaloneView::class); 82 $this->view->getRequest()->setControllerExtensionName('Viewpage'); 83 $this->view->setTemplate($templateName); 84 $this->view->setTemplateRootPaths(['EXT:viewpage/Resources/Private/Templates/ViewModule']); 85 $this->view->setPartialRootPaths(['EXT:viewpage/Resources/Private/Partials']); 86 $this->view->setLayoutRootPaths(['EXT:viewpage/Resources/Private/Layouts']); 87 } 88 89 /** 90 * Registers the docheader 91 * 92 * @param int $pageId 93 * @param int $languageId 94 * @param string $targetUrl 95 */ 96 protected function registerDocHeader(int $pageId, int $languageId, string $targetUrl) 97 { 98 $languages = $this->getPreviewLanguages($pageId); 99 if (count($languages) > 1) { 100 $languageMenu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu(); 101 $languageMenu->setIdentifier('_langSelector'); 102 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */ 103 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 104 foreach ($languages as $value => $label) { 105 $href = (string)$uriBuilder->buildUriFromRoute( 106 'web_ViewpageView', 107 [ 108 'id' => $pageId, 109 'language' => (int)$value 110 ] 111 ); 112 $menuItem = $languageMenu->makeMenuItem() 113 ->setTitle($label) 114 ->setHref($href); 115 if ($languageId === (int)$value) { 116 $menuItem->setActive(true); 117 } 118 $languageMenu->addMenuItem($menuItem); 119 } 120 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($languageMenu); 121 } 122 123 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); 124 $showButton = $buttonBar->makeLinkButton() 125 ->setHref($targetUrl) 126 ->setOnClick('window.open(this.href, \'newTYPO3frontendWindow\').focus();return false;') 127 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) 128 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-page', Icon::SIZE_SMALL)); 129 $buttonBar->addButton($showButton); 130 131 $refreshButton = $buttonBar->makeLinkButton() 132 ->setHref('javascript:document.getElementById(\'tx_viewpage_iframe\').contentWindow.location.reload(true);') 133 ->setTitle($this->getLanguageService()->sL('LLL:EXT:viewpage/Resources/Private/Language/locallang.xlf:refreshPage')) 134 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL)); 135 $buttonBar->addButton($refreshButton, ButtonBar::BUTTON_POSITION_RIGHT, 1); 136 137 // Shortcut 138 $mayMakeShortcut = $this->getBackendUser()->mayMakeShortcut(); 139 if ($mayMakeShortcut) { 140 $getVars = ['id', 'route']; 141 142 $shortcutButton = $buttonBar->makeShortcutButton() 143 ->setModuleName('web_ViewpageView') 144 ->setGetVariables($getVars); 145 $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); 146 } 147 } 148 149 /** 150 * Show selected page from pagetree in iframe 151 * 152 * @param ServerRequestInterface $request 153 * @return ResponseInterface 154 * @throws \TYPO3\CMS\Core\Exception 155 */ 156 public function showAction(ServerRequestInterface $request): ResponseInterface 157 { 158 $pageId = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0); 159 160 $this->initializeView('show'); 161 $this->moduleTemplate->setBodyTag('<body class="typo3-module-viewpage">'); 162 $this->moduleTemplate->setModuleName('typo3-module-viewpage'); 163 $this->moduleTemplate->setModuleId('typo3-module-viewpage'); 164 165 if (!$this->isValidDoktype($pageId)) { 166 $flashMessage = GeneralUtility::makeInstance( 167 FlashMessage::class, 168 $this->getLanguageService()->getLL('noValidPageSelected'), 169 '', 170 FlashMessage::INFO 171 ); 172 return $this->renderFlashMessage($flashMessage); 173 } 174 175 $languageId = $this->getCurrentLanguage($pageId, $request->getParsedBody()['language'] ?? $request->getQueryParams()['language'] ?? null); 176 try { 177 $targetUrl = BackendUtility::getPreviewUrl( 178 $pageId, 179 '', 180 null, 181 '', 182 '', 183 $this->getTypeParameterIfSet($pageId) . '&L=' . $languageId 184 ); 185 } catch (UnableToLinkToPageException $e) { 186 $flashMessage = GeneralUtility::makeInstance( 187 FlashMessage::class, 188 $this->getLanguageService()->getLL('noSiteConfiguration'), 189 '', 190 FlashMessage::WARNING 191 ); 192 return $this->renderFlashMessage($flashMessage); 193 } 194 195 $this->registerDocHeader($pageId, $languageId, $targetUrl); 196 197 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 198 $icons = []; 199 $icons['orientation'] = $iconFactory->getIcon('actions-device-orientation-change', Icon::SIZE_SMALL)->render('inline'); 200 $icons['fullscreen'] = $iconFactory->getIcon('actions-fullscreen', Icon::SIZE_SMALL)->render('inline'); 201 $icons['expand'] = $iconFactory->getIcon('actions-expand', Icon::SIZE_SMALL)->render('inline'); 202 $icons['desktop'] = $iconFactory->getIcon('actions-device-desktop', Icon::SIZE_SMALL)->render('inline'); 203 $icons['tablet'] = $iconFactory->getIcon('actions-device-tablet', Icon::SIZE_SMALL)->render('inline'); 204 $icons['mobile'] = $iconFactory->getIcon('actions-device-mobile', Icon::SIZE_SMALL)->render('inline'); 205 $icons['unidentified'] = $iconFactory->getIcon('actions-device-unidentified', Icon::SIZE_SMALL)->render('inline'); 206 207 $current = ($this->getBackendUser()->uc['moduleData']['web_view']['States']['current'] ?: []); 208 $current['label'] = ($current['label'] ?? $this->getLanguageService()->sL('LLL:EXT:viewpage/Resources/Private/Language/locallang.xlf:custom')); 209 $current['width'] = (isset($current['width']) && (int)$current['width'] >= 300 ? (int)$current['width'] : 320); 210 $current['height'] = (isset($current['height']) && (int)$current['height'] >= 300 ? (int)$current['height'] : 480); 211 212 $custom = ($this->getBackendUser()->uc['moduleData']['web_view']['States']['custom'] ?: []); 213 $custom['width'] = (isset($current['custom']) && (int)$current['custom'] >= 300 ? (int)$current['custom'] : 320); 214 $custom['height'] = (isset($current['custom']) && (int)$current['custom'] >= 300 ? (int)$current['custom'] : 480); 215 216 $this->view->assign('icons', $icons); 217 $this->view->assign('current', $current); 218 $this->view->assign('custom', $custom); 219 $this->view->assign('presetGroups', $this->getPreviewPresets($pageId)); 220 $this->view->assign('url', $targetUrl); 221 222 $this->moduleTemplate->setContent($this->view->render()); 223 return new HtmlResponse($this->moduleTemplate->renderContent()); 224 } 225 226 protected function renderFlashMessage(FlashMessage $flashMessage): HtmlResponse 227 { 228 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); 229 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); 230 $defaultFlashMessageQueue->enqueue($flashMessage); 231 232 $this->moduleTemplate->setContent($this->view->render()); 233 return new HtmlResponse($this->moduleTemplate->renderContent()); 234 } 235 236 /** 237 * With page TS config it is possible to force a specific type id via mod.web_view.type 238 * for a page id or a page tree. 239 * The method checks if a type is set for the given id and returns the additional GET string. 240 * 241 * @param int $pageId 242 * @return string 243 */ 244 protected function getTypeParameterIfSet(int $pageId): string 245 { 246 $typeParameter = ''; 247 $typeId = (int)(BackendUtility::getPagesTSconfig($pageId)['mod.']['web_view.']['type'] ?? 0); 248 if ($typeId > 0) { 249 $typeParameter = '&type=' . $typeId; 250 } 251 return $typeParameter; 252 } 253 254 /** 255 * Get available presets for page id 256 * 257 * @param int $pageId 258 * @return array 259 */ 260 protected function getPreviewPresets(int $pageId): array 261 { 262 $presetGroups = [ 263 'desktop' => [], 264 'tablet' => [], 265 'mobile' => [], 266 'unidentified' => [] 267 ]; 268 $previewFrameWidthConfig = BackendUtility::getPagesTSconfig($pageId)['mod.']['web_view.']['previewFrameWidths.'] ?? []; 269 foreach ($previewFrameWidthConfig as $item => $conf) { 270 $data = [ 271 'key' => substr($item, 0, -1), 272 'label' => $conf['label'] ?? null, 273 'type' => $conf['type'] ?? 'unknown', 274 'width' => (isset($conf['width']) && (int)$conf['width'] > 0 && strpos($conf['width'], '%') === false) ? (int)$conf['width'] : null, 275 'height' => (isset($conf['height']) && (int)$conf['height'] > 0 && strpos($conf['height'], '%') === false) ? (int)$conf['height'] : null, 276 ]; 277 $width = (int)substr($item, 0, -1); 278 if (!isset($data['width']) && $width > 0) { 279 $data['width'] = $width; 280 } 281 if (!isset($data['label'])) { 282 $data['label'] = $data['key']; 283 } elseif (strpos($data['label'], 'LLL:') === 0) { 284 $data['label'] = $this->getLanguageService()->sL(trim($data['label'])); 285 } 286 287 if (array_key_exists($data['type'], $presetGroups)) { 288 $presetGroups[$data['type']][$data['key']] = $data; 289 } else { 290 $presetGroups['unidentified'][$data['key']] = $data; 291 } 292 } 293 294 return $presetGroups; 295 } 296 297 /** 298 * Returns the preview languages 299 * 300 * @param int $pageId 301 * @return array 302 */ 303 protected function getPreviewLanguages(int $pageId): array 304 { 305 $languages = []; 306 $modSharedTSconfig = BackendUtility::getPagesTSconfig($pageId)['mod.']['SHARED.'] ?? []; 307 if ($modSharedTSconfig['view.']['disableLanguageSelector'] === '1') { 308 return $languages; 309 } 310 311 try { 312 $pageRepository = GeneralUtility::makeInstance(PageRepository::class); 313 $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageId); 314 $siteLanguages = $site->getAvailableLanguages($this->getBackendUser(), false, $pageId); 315 316 foreach ($siteLanguages as $siteLanguage) { 317 $languageAspectToTest = LanguageAspectFactory::createFromSiteLanguage($siteLanguage); 318 $page = $pageRepository->getPageOverlay($pageRepository->getPage($pageId), $siteLanguage->getLanguageId()); 319 320 if ($pageRepository->isPageSuitableForLanguage($page, $languageAspectToTest)) { 321 $languages[$siteLanguage->getLanguageId()] = $siteLanguage->getTitle(); 322 } 323 } 324 } catch (SiteNotFoundException $e) { 325 // do nothing 326 } 327 return $languages; 328 } 329 330 /** 331 * Returns the current language 332 * 333 * @param int $pageId 334 * @param string $languageParam 335 * @return int 336 */ 337 protected function getCurrentLanguage(int $pageId, string $languageParam = null): int 338 { 339 $languageId = (int)$languageParam; 340 if ($languageParam === null) { 341 $states = $this->getBackendUser()->uc['moduleData']['web_view']['States']; 342 $languages = $this->getPreviewLanguages($pageId); 343 if (isset($states['languageSelectorValue']) && isset($languages[$states['languageSelectorValue']])) { 344 $languageId = (int)$states['languageSelectorValue']; 345 } 346 } else { 347 $this->getBackendUser()->uc['moduleData']['web_view']['States']['languageSelectorValue'] = $languageId; 348 $this->getBackendUser()->writeUC($this->getBackendUser()->uc); 349 } 350 return $languageId; 351 } 352 353 /** 354 * Verifies if doktype of given page is valid 355 * 356 * @param int $pageId 357 * @return bool 358 */ 359 protected function isValidDoktype(int $pageId = 0): bool 360 { 361 if ($pageId === 0) { 362 return false; 363 } 364 365 $page = BackendUtility::getRecord('pages', $pageId); 366 $pageType = (int)($page['doktype'] ?? 0); 367 368 return $pageType !== 0 369 && !in_array($pageType, [ 370 PageRepository::DOKTYPE_SPACER, 371 PageRepository::DOKTYPE_SYSFOLDER, 372 PageRepository::DOKTYPE_RECYCLER 373 ], true); 374 } 375 376 /** 377 * @return BackendUserAuthentication 378 */ 379 protected function getBackendUser(): BackendUserAuthentication 380 { 381 return $GLOBALS['BE_USER']; 382 } 383 384 /** 385 * @return LanguageService 386 */ 387 protected function getLanguageService(): LanguageService 388 { 389 return $GLOBALS['LANG']; 390 } 391} 392