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