‪TYPO3CMS  10.4
WorkspacePreview.php
Go to the documentation of this file.
1 <?php
2 
3 declare(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 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use Psr\Http\Message\UriInterface;
23 use Psr\Http\Server\MiddlewareInterface;
24 use Psr\Http\Server\RequestHandlerInterface;
25 use Symfony\Component\HttpFoundation\Cookie;
41 
50 class ‪WorkspacePreview implements MiddlewareInterface
51 {
53 
59  protected ‪$previewKey = 'ADMCMD_prev';
60 
74  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
75  {
76  $addInformationAboutDisabledCache = false;
77  $keyword = $this->‪getPreviewInputCode($request);
78  $setCookieOnCurrentRequest = false;
80  $normalizedParams = $request->getAttribute('normalizedParams');
81  $context = GeneralUtility::makeInstance(Context::class);
82 
83  // First, if a Log out is happening, a custom HTML output page is shown and the request exits with removing
84  // the cookie for the backend preview.
85  if ($keyword === 'LOGOUT') {
86  // "log out", and unset the cookie
87  $message = $this->‪getLogoutTemplateMessage($request->getUri());
88  $response = new ‪HtmlResponse($message);
89  return $this->‪addCookie('', $normalizedParams, $response);
90  }
91 
92  // If the keyword is ignore, then the preview is not managed as "Preview User" but handled
93  // via the regular backend user or even no user if the GET parameter ADMCMD_noBeUser is set
94  if (!empty($keyword) && $keyword !== 'IGNORE' && $keyword !== 'LIVE') {
95  $routeResult = $request->getAttribute('routing', null);
96  // A keyword was found in a query parameter or in a cookie
97  // If the keyword is valid, activate a BE User and override any existing BE Users
98  // (in case workspace ID was given and a corresponding site to be used was found)
99  $previewWorkspaceId = (int)$this->‪getWorkspaceIdFromRequest($request, $keyword);
100  if ($previewWorkspaceId > 0 && $routeResult instanceof ‪RouteResultInterface) {
101  $previewUser = $this->‪initializePreviewUser($previewWorkspaceId);
102  if ($previewUser instanceof ‪PreviewUserAuthentication) {
103  ‪$GLOBALS['BE_USER'] = $previewUser;
104  // Register the preview user as aspect
105  $this->‪setBackendUserAspect($context, $previewUser);
106  // If the GET parameter is set, and we have a valid Preview User, the cookie needs to be
107  // set and the GET parameter should be removed.
108  $setCookieOnCurrentRequest = $request->getQueryParams()[‪$this->previewKey] ?? false;
109  }
110  }
111  }
112 
113  // If keyword is set to "LIVE", then ensure that there is no workspace preview, but keep the BE User logged in.
114  // This option is solely used to ensure that a be user can preview the live version of a page in the
115  // workspace preview module.
116  if ($keyword === 'LIVE' && ‪$GLOBALS['BE_USER'] instanceof ‪FrontendBackendUserAuthentication) {
117  // We need to set the workspace to live here
118  ‪$GLOBALS['BE_USER']->setTemporaryWorkspace(0);
119  // Register the backend user as aspect
120  $this->‪setBackendUserAspect($context, ‪$GLOBALS['BE_USER']);
121  // Caching is disabled, because otherwise generated URLs could include the keyword parameter
122  $request = $request->withAttribute('noCache', true);
123  $addInformationAboutDisabledCache = true;
124  $setCookieOnCurrentRequest = false;
125  }
126 
127  $response = $handler->handle($request);
128 
129  if (‪$GLOBALS['TSFE'] instanceof ‪TypoScriptFrontendController && $addInformationAboutDisabledCache) {
130  ‪$GLOBALS['TSFE']->set_no_cache('GET Parameter ADMCMD_prev=LIVE was given', true);
131  }
132 
133  // Add an info box to the frontend content
134  if (‪$GLOBALS['TSFE'] instanceof ‪TypoScriptFrontendController && ‪$GLOBALS['TSFE']->isOutputting(true) && $context->getPropertyFromAspect('workspace', 'isOffline', false)) {
135  $previewInfo = $this->‪renderPreviewInfo(‪$GLOBALS['TSFE'], $request->getUri());
136  $body = $response->getBody();
137  $body->rewind();
138  $content = $body->getContents();
139  $content = str_ireplace('</body>', $previewInfo . '</body>', $content);
140  $body = new ‪Stream('php://temp', 'rw');
141  $body->write($content);
142  $response = $response->withBody($body);
143  }
144 
145  // If the GET parameter ADMCMD_prev is set, then a cookie is set for the next request to keep the preview user
146  if ($setCookieOnCurrentRequest) {
147  $response = $this->‪addCookie($keyword, $normalizedParams, $response);
148  }
149  return $response;
150  }
151 
159  protected function ‪getLogoutTemplateMessage(UriInterface $currentUrl): string
160  {
161  $currentUrl = $this->‪removePreviewParameterFromUrl($currentUrl);
162  if (‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
163  $templateFile = GeneralUtility::getFileAbsFileName(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']);
164  if (@is_file($templateFile)) {
165  $message = (string)file_get_contents($templateFile);
166  } else {
167  $message = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutError');
168  $message = htmlspecialchars($message);
169  $message = sprintf($message, '<strong>', '</strong><br>', $templateFile);
170  }
171  } else {
172  $message = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutSuccess');
173  $message = htmlspecialchars($message);
174  $message = sprintf($message, '<a href="' . htmlspecialchars((string)$currentUrl) . '">', '</a>');
175  }
176  return sprintf($message, htmlspecialchars((string)$currentUrl));
177  }
178 
195  protected function ‪getWorkspaceIdFromRequest(ServerRequestInterface $request, string $inputCode): ?int
196  {
197  $previewData = $this->‪getPreviewData($inputCode);
198  if (!is_array($previewData)) {
199  // ADMCMD command could not be executed! (No keyword configuration found)
200  return null;
201  }
202  if ($request->getMethod() === 'POST') {
203  throw new \Exception('POST requests are incompatible with keyword preview.', 1294585191);
204  }
205  // Validate configuration
206  $previewConfig = json_decode($previewData['config'], true);
207  if (!$previewConfig['fullWorkspace']) {
208  throw new \Exception('Preview configuration did not include a workspace preview', 1294585190);
209  }
210  return (int)$previewConfig['fullWorkspace'];
211  }
212 
219  protected function ‪initializePreviewUser(int $workspaceUid): ?‪PreviewUserAuthentication
220  {
221  $previewUser = GeneralUtility::makeInstance(PreviewUserAuthentication::class);
222  if ($previewUser->setTemporaryWorkspace($workspaceUid)) {
223  return $previewUser;
224  }
225  return null;
226  }
227 
236  protected function ‪addCookie(string $keyword, ‪NormalizedParams $normalizedParams, ResponseInterface $response): ResponseInterface
237  {
238  $cookieSameSite = $this->sanitizeSameSiteCookieValue(
239  strtolower(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite'] ?? Cookie::SAMESITE_STRICT)
240  );
241  // None needs the secure option (only allowed on HTTPS)
242  $cookieSecure = $cookieSameSite === Cookie::SAMESITE_NONE || $normalizedParams->‪isHttps();
243 
244  $cookie = new Cookie(
245  $this->previewKey,
246  $keyword,
247  0,
248  $normalizedParams->‪getSitePath(),
249  null,
250  $cookieSecure,
251  true,
252  false,
253  $cookieSameSite
254  );
255  return $response->withAddedHeader('Set-Cookie', $cookie->__toString());
256  }
257 
265  protected function ‪getPreviewInputCode(ServerRequestInterface $request): string
266  {
267  return $request->getQueryParams()[‪$this->previewKey] ?? $request->getCookieParams()[‪$this->previewKey] ?? '';
268  }
269 
276  protected function ‪getPreviewData(string $keyword)
277  {
278  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
279  ->getQueryBuilderForTable('sys_preview');
280  return $queryBuilder
281  ->select('*')
282  ->from('sys_preview')
283  ->where(
284  $queryBuilder->expr()->eq(
285  'keyword',
286  $queryBuilder->createNamedParameter($keyword)
287  ),
288  $queryBuilder->expr()->gt(
289  'endtime',
290  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
291  )
292  )
293  ->setMaxResults(1)
294  ->execute()
295  ->fetch();
296  }
297 
317  protected function ‪renderPreviewInfo(‪TypoScriptFrontendController $tsfe, UriInterface $currentUrl): string
318  {
319  $content = '';
320  if (!isset($tsfe->config['config']['disablePreviewNotification']) || (int)$tsfe->config['config']['disablePreviewNotification'] !== 1) {
321  // get the title of the current workspace
322  $currentWorkspaceId = $tsfe->‪whichWorkspace();
323  $currentWorkspaceTitle = $this->‪getWorkspaceTitle($currentWorkspaceId);
324  $currentWorkspaceTitle = htmlspecialchars($currentWorkspaceTitle);
325  if ($tsfe->config['config']['message_preview_workspace']) {
326  $content = sprintf(
327  $tsfe->config['config']['message_preview_workspace'],
328  $currentWorkspaceTitle,
329  $currentWorkspaceId ?? -99
330  );
331  } else {
332  $text = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewText');
333  $text = htmlspecialchars($text);
334  $text = sprintf($text, $currentWorkspaceTitle, $currentWorkspaceId ?? -99);
335  $stopPreviewText = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stopPreview');
336  $stopPreviewText = htmlspecialchars($stopPreviewText);
337  if (‪$GLOBALS['BE_USER'] instanceof ‪PreviewUserAuthentication) {
338  $urlForStoppingPreview = (string)$this->‪removePreviewParameterFromUrl($currentUrl, 'LOGOUT');
339  $text .= '<br><a style="color: #000; pointer-events: visible;" href="' . htmlspecialchars($urlForStoppingPreview) . '">' . $stopPreviewText . '</a>';
340  }
341  $styles = [];
342  $styles[] = 'position: fixed';
343  $styles[] = 'top: 15px';
344  $styles[] = 'right: 15px';
345  $styles[] = 'padding: 8px 18px';
346  $styles[] = 'background: #fff3cd';
347  $styles[] = 'border: 1px solid #ffeeba';
348  $styles[] = 'font-family: sans-serif';
349  $styles[] = 'font-size: 14px';
350  $styles[] = 'font-weight: bold';
351  $styles[] = 'color: #856404';
352  $styles[] = 'z-index: 20000';
353  $styles[] = 'user-select: none';
354  $styles[] = 'pointer-events: none';
355  $styles[] = 'text-align: center';
356  $styles[] = 'border-radius: 2px';
357  $content = '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . $text . '</div>';
358  }
359  }
360  return $content;
361  }
362 
369  protected function ‪getWorkspaceTitle(int $workspaceId): string
370  {
371  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
372  ->getQueryBuilderForTable('sys_workspace');
373  $title = $queryBuilder
374  ->select('title')
375  ->from('sys_workspace')
376  ->where(
377  $queryBuilder->expr()->eq(
378  'uid',
379  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
380  )
381  )
382  ->execute()
383  ->fetchColumn();
384  return (string)($title !== false ? $title : '');
385  }
386 
394  protected function ‪removePreviewParameterFromUrl(UriInterface $url, string $newAdminCommand = ''): UriInterface
395  {
396  $queryString = $url->getQuery();
397  if (!empty($queryString)) {
398  $queryStringParts = GeneralUtility::explodeUrl2Array($queryString);
399  unset($queryStringParts[$this->previewKey]);
400  } else {
401  $queryStringParts = [];
402  }
403  if ($newAdminCommand !== '') {
404  $queryStringParts[‪$this->previewKey] = $newAdminCommand;
405  }
406  $queryString = http_build_query($queryStringParts, '', '&', PHP_QUERY_RFC3986);
407  return $url->withQuery($queryString);
408  }
409 
413  protected function ‪getLanguageService(): ‪LanguageService
414  {
415  return ‪$GLOBALS['LANG'] ?: ‪LanguageService::create('default');
416  }
417 
424  protected function ‪setBackendUserAspect(‪Context $context, ‪BackendUserAuthentication $user = null)
425  {
426  $context->‪setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
427  $context->‪setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0));
428  }
429 }
‪TYPO3\CMS\Core\Context\WorkspaceAspect
Definition: WorkspaceAspect.php:31
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLogoutTemplateMessage
‪string getLogoutTemplateMessage(UriInterface $currentUrl)
Definition: WorkspacePreview.php:158
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewData
‪mixed getPreviewData(string $keyword)
Definition: WorkspacePreview.php:275
‪TYPO3\CMS\Core\Http\NormalizedParams\getSitePath
‪string getSitePath()
Definition: NormalizedParams.php:445
‪TYPO3\CMS\Core\Routing\RouteResultInterface
Definition: RouteResultInterface.php:24
‪TYPO3\CMS\Backend\FrontendBackendUserAuthentication
Definition: FrontendBackendUserAuthentication.php:31
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\setBackendUserAspect
‪setBackendUserAspect(Context $context, BackendUserAuthentication $user=null)
Definition: WorkspacePreview.php:423
‪TYPO3\CMS\Workspaces\Middleware
Definition: WorkspacePreview.php:18
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getWorkspaceIdFromRequest
‪int null getWorkspaceIdFromRequest(ServerRequestInterface $request, string $inputCode)
Definition: WorkspacePreview.php:194
‪TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication
Definition: PreviewUserAuthentication.php:45
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\renderPreviewInfo
‪string renderPreviewInfo(TypoScriptFrontendController $tsfe, UriInterface $currentUrl)
Definition: WorkspacePreview.php:316
‪TYPO3\CMS\Core\Localization\LanguageService\sL
‪string sL($input)
Definition: LanguageService.php:194
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:162
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\$previewKey
‪string $previewKey
Definition: WorkspacePreview.php:58
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\initializePreviewUser
‪PreviewUserAuthentication null initializePreviewUser(int $workspaceUid)
Definition: WorkspacePreview.php:218
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\removePreviewParameterFromUrl
‪UriInterface removePreviewParameterFromUrl(UriInterface $url, string $newAdminCommand='')
Definition: WorkspacePreview.php:393
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getWorkspaceTitle
‪string getWorkspaceTitle(int $workspaceId)
Definition: WorkspacePreview.php:368
‪TYPO3\CMS\Core\Http\Stream
Definition: Stream.php:29
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Http\NormalizedParams\isHttps
‪bool isHttps()
Definition: NormalizedParams.php:333
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewInputCode
‪string getPreviewInputCode(ServerRequestInterface $request)
Definition: WorkspacePreview.php:264
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview
Definition: WorkspacePreview.php:51
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLanguageService
‪LanguageService getLanguageService()
Definition: WorkspacePreview.php:412
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController\whichWorkspace
‪int whichWorkspace()
Definition: TypoScriptFrontendController.php:3429
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:98
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\addCookie
‪ResponseInterface addCookie(string $keyword, NormalizedParams $normalizedParams, ResponseInterface $response)
Definition: WorkspacePreview.php:235
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:42
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\process
‪ResponseInterface process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: WorkspacePreview.php:73
‪TYPO3\CMS\Core\Context\UserAspect
Definition: UserAspect.php:38
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:35
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:26
‪TYPO3\CMS\Core\Localization\LanguageService\create
‪static static create(string $locale)
Definition: LanguageService.php:430