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