‪TYPO3CMS  ‪main
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;
45 
54 final class ‪WorkspacePreview implements MiddlewareInterface
55 {
57 
61  private const ‪PREVIEW_KEY = 'ADMCMD_prev';
62 
63  private bool ‪$previewNotificationEnabled = false;
64  private ?string ‪$previewMessage = null;
65 
66  public function ‪__construct(private readonly ‪Context $context) {}
67 
76  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
77  {
78  $keyword = $this->‪getPreviewInputCode($request);
79  $setCookieOnCurrentRequest = false;
80  $normalizedParams = $request->getAttribute('normalizedParams');
81 
82  // First, if a Log-out is happening, a custom HTML output page is shown and the request exits with removing
83  // the cookie for the backend preview.
84  if ($keyword === 'LOGOUT') {
85  // "log out", and unset the cookie
86  $message = $this->‪getLogoutTemplateMessage($request->getUri());
87  $response = new ‪HtmlResponse($message);
88  return $this->‪addCookie('', $normalizedParams, $response);
89  }
90 
91  // If the keyword is "IGNORE", then the preview is not managed as "Preview User" but handled
92  // via the regular backend user or even no user if the GET parameter ADMCMD_noBeUser is set
93  if (!empty($keyword) && $keyword !== 'IGNORE' && $keyword !== 'LIVE') {
94  $routeResult = $request->getAttribute('routing', null);
95  // A keyword was found in a query parameter or in a cookie
96  // If the keyword is valid, activate a BE User and override any existing BE Users
97  // (in case workspace ID was given and a corresponding site to be used was found)
98  $previewWorkspaceId = (int)$this->‪getWorkspaceIdFromRequest($keyword);
99  if ($previewWorkspaceId > 0 && $routeResult instanceof ‪RouteResultInterface) {
100  $previewUser = $this->‪initializePreviewUser($previewWorkspaceId);
101  if ($previewUser !== null) {
102  ‪$GLOBALS['BE_USER'] = $previewUser;
103  // Register the preview user as aspect
104  $this->‪setBackendUserAspect($previewUser);
105  // If the GET parameter is set, and we have a valid Preview User, the cookie needs to be
106  // set and the GET parameter should be removed.
107  $setCookieOnCurrentRequest = $request->getQueryParams()[‪self::PREVIEW_KEY] ?? false;
108  }
109  }
110  }
111 
112  // If keyword is set to "LIVE", then ensure that there is no workspace preview, but keep the BE User logged in.
113  // This option is solely used to ensure that a be-user can preview the live version of a page in the
114  // workspace preview module.
115  if ($keyword === 'LIVE' && isset(‪$GLOBALS['BE_USER']) && ‪$GLOBALS['BE_USER'] instanceof ‪FrontendBackendUserAuthentication) {
116  // We need to set the workspace to "live" here
117  ‪$GLOBALS['BE_USER']->setTemporaryWorkspace(0);
118  // Register the backend user as aspect
119  $this->‪setBackendUserAspect(‪$GLOBALS['BE_USER']);
120  $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new ‪CacheInstruction());
121  $cacheInstruction->disableCache('EXT:workspaces: Disabled FE cache with BE_USER previewing live workspace');
122  $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction);
123  $setCookieOnCurrentRequest = false;
124  }
125 
126  $response = $handler->handle($request);
127 
128  // Add an info box to the frontend content
129  if ($this->context->getPropertyFromAspect('workspace', 'isOffline', false)) {
130  $previewInfo = $this->‪renderPreviewInfo($request->getUri());
131  $body = $response->getBody();
132  $body->rewind();
133  $content = $body->getContents();
134  $content = str_ireplace('</body>', $previewInfo . '</body>', $content);
135  $body = new ‪Stream('php://temp', 'rw');
136  $body->write($content);
137  $response = $response->withBody($body);
138  }
139 
140  // If the GET parameter ADMCMD_prev is set, then a cookie is set for the next request to keep the preview user
141  if ($setCookieOnCurrentRequest) {
142  $response = $this->‪addCookie($keyword, $normalizedParams, $response);
143  }
144  return $response;
145  }
146 
154  #[AsEventListener('typo3-workspaces/workspace-preview-middleware')]
156  {
157  $typoScriptConfig = $event->‪getFrontendTypoScript()->getConfigArray();
158  if (!isset($typoScriptConfig['disablePreviewNotification']) || (int)$typoScriptConfig['disablePreviewNotification'] !== 1) {
159  $this->previewNotificationEnabled = true;
160  }
161  if ($typoScriptConfig['message_preview_workspace'] ?? false) {
162  $this->previewMessage = $typoScriptConfig['message_preview_workspace'];
163  }
164  }
165 
170  private function ‪getLogoutTemplateMessage(UriInterface $currentUrl): string
171  {
172  $currentUrl = $this->‪removePreviewParameterFromUrl($currentUrl);
173  if (‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
174  $templateFile = GeneralUtility::getFileAbsFileName(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']);
175  if (@is_file($templateFile)) {
176  $message = (string)file_get_contents($templateFile);
177  } else {
178  $message = $this->‪getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutError');
179  $message = htmlspecialchars($message);
180  $message = sprintf($message, '<strong>', '</strong><br>', $templateFile);
181  }
182  } else {
183  $message = $this->‪getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutSuccess');
184  $message = htmlspecialchars($message);
185  $message = sprintf($message, '<a href="' . htmlspecialchars((string)$currentUrl) . '">', '</a>');
186  }
187  return sprintf($message, htmlspecialchars((string)$currentUrl));
188  }
189 
204  private function ‪getWorkspaceIdFromRequest(string $inputCode): ?int
205  {
206  $previewData = $this->‪getPreviewData($inputCode);
207  if (!is_array($previewData)) {
208  // ADMCMD command could not be executed! (No keyword configuration found)
209  return null;
210  }
211  // Validate configuration
212  $previewConfig = json_decode($previewData['config'] ?? '', true);
213  if (!isset($previewConfig['fullWorkspace']) || !$previewConfig['fullWorkspace']) {
214  throw new \Exception('Preview configuration did not include a workspace preview', 1294585190);
215  }
216  return (int)$previewConfig['fullWorkspace'];
217  }
218 
225  private function ‪initializePreviewUser(int $workspaceUid): ?‪PreviewUserAuthentication
226  {
227  $previewUser = GeneralUtility::makeInstance(PreviewUserAuthentication::class);
228  if ($previewUser->setTemporaryWorkspace($workspaceUid)) {
229  return $previewUser;
230  }
231  return null;
232  }
233 
237  private function ‪addCookie(string $keyword, ‪NormalizedParams $normalizedParams, ResponseInterface $response): ResponseInterface
238  {
239  $cookieSameSite = $this->sanitizeSameSiteCookieValue(
240  strtolower(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite'] ?? Cookie::SAMESITE_STRICT)
241  );
242  // SameSite Cookie = None needs the secure option (only allowed on HTTPS)
243  $isSecure = $cookieSameSite === Cookie::SAMESITE_NONE || $normalizedParams->‪isHttps();
244 
245  $cookie = new Cookie(
246  self::PREVIEW_KEY,
247  $keyword,
248  0,
249  $normalizedParams->‪getSitePath(),
250  null,
251  $isSecure,
252  true,
253  false,
254  $cookieSameSite
255  );
256  return $response->withAddedHeader('Set-Cookie', $cookie->__toString());
257  }
258 
263  private function ‪getPreviewInputCode(ServerRequestInterface $request): string
264  {
265  return $request->getQueryParams()[‪self::PREVIEW_KEY] ?? $request->getCookieParams()[‪self::PREVIEW_KEY] ?? '';
266  }
267 
273  private function ‪getPreviewData(string $keyword)
274  {
275  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_preview');
276  return $queryBuilder
277  ->select('*')
278  ->from('sys_preview')
279  ->where(
280  $queryBuilder->expr()->eq('keyword', $queryBuilder->createNamedParameter($keyword)),
281  $queryBuilder->expr()->gt('endtime', $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT))
282  )
283  ->setMaxResults(1)
284  ->executeQuery()
285  ->fetchAssociative();
286  }
287 
295  private function ‪renderPreviewInfo(UriInterface $currentUrl): string
296  {
297  $content = '';
298  if ($this->previewNotificationEnabled) {
299  // get the title of the current workspace
300  $currentWorkspaceId = $this->context->getPropertyFromAspect('workspace', 'id', 0);
301  $currentWorkspaceTitle = $this->‪getWorkspaceTitle($currentWorkspaceId);
302  $currentWorkspaceTitle = htmlspecialchars($currentWorkspaceTitle);
303  if ($this->previewMessage !== null) {
304  $content = sprintf(
305  $this->previewMessage,
306  $currentWorkspaceTitle,
307  $currentWorkspaceId
308  );
309  } else {
310  $text = $this->‪getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewText');
311  $text = htmlspecialchars($text);
312  $text = sprintf($text, $currentWorkspaceTitle, $currentWorkspaceId);
313  $stopPreviewText = $this->‪getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stopPreview');
314  $stopPreviewText = htmlspecialchars($stopPreviewText);
315  if (‪$GLOBALS['BE_USER'] instanceof ‪PreviewUserAuthentication) {
316  $urlForStoppingPreview = (string)$this->‪removePreviewParameterFromUrl($currentUrl, 'LOGOUT');
317  $text .= '<br><a style="color: #000; pointer-events: visible;" href="' . htmlspecialchars($urlForStoppingPreview) . '">' . $stopPreviewText . '</a>';
318  }
319  $styles = [];
320  $styles[] = 'position: fixed';
321  $styles[] = 'top: 15px';
322  $styles[] = 'right: 15px';
323  $styles[] = 'padding: 8px 18px';
324  $styles[] = 'background: #fff3cd';
325  $styles[] = 'border: 1px solid #ffeeba';
326  $styles[] = 'font-family: sans-serif';
327  $styles[] = 'font-size: 14px';
328  $styles[] = 'font-weight: bold';
329  $styles[] = 'color: #856404';
330  $styles[] = 'z-index: 20000';
331  $styles[] = 'user-select: none';
332  $styles[] = 'pointer-events: none';
333  $styles[] = 'text-align: center';
334  $styles[] = 'border-radius: 2px';
335  $content = '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . $text . '</div>';
336  }
337  }
338  return $content;
339  }
340 
344  private function ‪getWorkspaceTitle(int $workspaceId): string
345  {
346  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
347  $title = $queryBuilder
348  ->select('title')
349  ->from('sys_workspace')
350  ->where(
351  $queryBuilder->expr()->eq(
352  'uid',
353  $queryBuilder->createNamedParameter($workspaceId, ‪Connection::PARAM_INT)
354  )
355  )
356  ->executeQuery()
357  ->fetchOne();
358  return (string)($title !== false ? $title : '');
359  }
360 
364  private function ‪removePreviewParameterFromUrl(UriInterface ‪$url, string $newAdminCommand = ''): UriInterface
365  {
366  $queryString = ‪$url->getQuery();
367  if (!empty($queryString)) {
368  $queryStringParts = GeneralUtility::explodeUrl2Array($queryString);
369  unset($queryStringParts[self::PREVIEW_KEY]);
370  } else {
371  $queryStringParts = [];
372  }
373  if ($newAdminCommand !== '') {
374  $queryStringParts[‪self::PREVIEW_KEY] = $newAdminCommand;
375  }
376  $queryString = http_build_query($queryStringParts, '', '&', PHP_QUERY_RFC3986);
377  return ‪$url->withQuery($queryString);
378  }
379 
381  {
382  return ‪$GLOBALS['LANG'] ?? GeneralUtility::makeInstance(LanguageServiceFactory::class)->create('default');
383  }
384 
389  {
390  $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
391  $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0));
392  }
393 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Context\WorkspaceAspect
Definition: WorkspaceAspect.php:31
‪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:452
‪TYPO3\CMS\Core\Routing\RouteResultInterface
Definition: RouteResultInterface.php:23
‪TYPO3\CMS\Backend\FrontendBackendUserAuthentication
Definition: FrontendBackendUserAuthentication.php:29
‪TYPO3\CMS\Core\Attribute\AsEventListener
Definition: AsEventListener.php:25
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getWorkspaceIdFromRequest
‪int null getWorkspaceIdFromRequest(string $inputCode)
Definition: WorkspacePreview.php:204
‪TYPO3\CMS\Workspaces\Middleware
Definition: WorkspacePreview.php:18
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\addCookie
‪addCookie(string $keyword, NormalizedParams $normalizedParams, ResponseInterface $response)
Definition: WorkspacePreview.php:237
‪TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication
Definition: PreviewUserAuthentication.php:46
‪TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent\getFrontendTypoScript
‪getFrontendTypoScript()
Definition: AfterTypoScriptDeterminedEvent.php:46
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\typoScriptDeterminedListener
‪typoScriptDeterminedListener(AfterTypoScriptDeterminedEvent $event)
Definition: WorkspacePreview.php:155
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewInputCode
‪getPreviewInputCode(ServerRequestInterface $request)
Definition: WorkspacePreview.php:263
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\initializePreviewUser
‪PreviewUserAuthentication null initializePreviewUser(int $workspaceUid)
Definition: WorkspacePreview.php:225
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getWorkspaceTitle
‪getWorkspaceTitle(int $workspaceId)
Definition: WorkspacePreview.php:344
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLogoutTemplateMessage
‪getLogoutTemplateMessage(UriInterface $currentUrl)
Definition: WorkspacePreview.php:170
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\setBackendUserAspect
‪setBackendUserAspect(BackendUserAuthentication $user=null)
Definition: WorkspacePreview.php:388
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\removePreviewParameterFromUrl
‪removePreviewParameterFromUrl(UriInterface $url, string $newAdminCommand='')
Definition: WorkspacePreview.php:364
‪TYPO3\CMS\Core\Http\Stream
Definition: Stream.php:31
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Http\NormalizedParams\isHttps
‪bool isHttps()
Definition: NormalizedParams.php:340
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\$previewMessage
‪string $previewMessage
Definition: WorkspacePreview.php:64
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview
Definition: WorkspacePreview.php:55
‪TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent
Definition: AfterTypoScriptDeterminedEvent.php:41
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\__construct
‪__construct(private readonly Context $context)
Definition: WorkspacePreview.php:66
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Frontend\Cache\CacheInstruction
Definition: CacheInstruction.php:29
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\PREVIEW_KEY
‪const PREVIEW_KEY
Definition: WorkspacePreview.php:61
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: WorkspacePreview.php:76
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLanguageService
‪getLanguageService()
Definition: WorkspacePreview.php:380
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\$previewNotificationEnabled
‪bool $previewNotificationEnabled
Definition: WorkspacePreview.php:63
‪TYPO3\CMS\Core\Context\UserAspect
Definition: UserAspect.php:37
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:38
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:28
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\renderPreviewInfo
‪renderPreviewInfo(UriInterface $currentUrl)
Definition: WorkspacePreview.php:295