‪TYPO3CMS  9.5
WorkspacePreview.php
Go to the documentation of this file.
1 <?php
2 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 
18 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use Psr\Http\Server\MiddlewareInterface;
21 use Psr\Http\Server\RequestHandlerInterface;
22 use Symfony\Component\HttpFoundation\Cookie;
36 
45 class ‪WorkspacePreview implements MiddlewareInterface
46 {
48 
54  protected ‪$previewKey = 'ADMCMD_prev';
55 
69  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
70  {
71  $keyword = $this->‪getPreviewInputCode($request);
72  if ($keyword) {
73  switch ($keyword) {
74  case 'IGNORE':
75  break;
76  case 'LOGOUT':
77  // "log out", and unset the cookie
78  $this->‪setCookie('', $request->getAttribute('normalizedParams'));
79  $message = $this->‪getLogoutTemplateMessage($request->getQueryParams()['returnUrl'] ?? '');
80  return new ‪HtmlResponse($message);
81  default:
82  // A keyword was found in a query parameter or in a cookie
83  // If the keyword is valid, activate a BE User and override any existing BE Users
84  $configuration = $this->‪getPreviewConfigurationFromRequest($request, $keyword);
85  if (is_array($configuration) && $configuration['fullWorkspace'] > 0) {
86  $previewUser = $this->‪initializePreviewUser(
87  (int)$configuration['fullWorkspace'],
88  ‪$GLOBALS['TSFE']->id
89  );
90  if ($previewUser) {
91  ‪$GLOBALS['BE_USER'] = $previewUser;
92  // Register the preview user as aspect
93  $this->‪setBackendUserAspect(GeneralUtility::makeInstance(Context::class), $previewUser);
94  }
95  }
96  }
97  }
98 
99  // If "ADMCMD_noBeUser" is set, then ensure that there is no workspace preview and no BE User logged in.
100  // This option is solely used to ensure that a be user can preview the live version of a page in the
101  // workspace preview module.
102  if ($request->getQueryParams()['ADMCMD_noBeUser']) {
103  ‪$GLOBALS['BE_USER'] = null;
104  // Register the backend user as aspect
105  $this->‪setBackendUserAspect(GeneralUtility::makeInstance(Context::class), null);
106  // Caching is disabled, because otherwise generated URLs could include the ADMCMD_noBeUser parameter
107  ‪$GLOBALS['TSFE']->set_no_cache('GET Parameter ADMCMD_noBeUser was given', true);
108  }
109 
110  $response = $handler->handle($request);
111 
112  // Add a info box to the frontend content
113  if (‪$GLOBALS['TSFE']->doWorkspacePreview() && ‪$GLOBALS['TSFE']->isOutputting()) {
114  $previewInfo = $this->‪renderPreviewInfo(‪$GLOBALS['TSFE'], $request->getAttribute('normalizedParams'));
115  $body = $response->getBody();
116  $body->rewind();
117  $content = $body->getContents();
118  $content = str_ireplace('</body>', $previewInfo . '</body>', $content);
119  $body = new ‪Stream('php://temp', 'rw');
120  $body->write($content);
121  $response = $response->withBody($body);
122  }
123 
124  return $response;
125  }
126 
134  protected function ‪getLogoutTemplateMessage(string $returnUrl = ''): string
135  {
136  $returnUrl = GeneralUtility::sanitizeLocalUrl($returnUrl);
137  $returnUrl = $this->‪removePreviewParameterFromUrl($returnUrl);
138  if (‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
139  $templateFile = GeneralUtility::getFileAbsFileName(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']);
140  if (@is_file($templateFile)) {
141  $message = file_get_contents($templateFile);
142  } else {
143  $message = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutError');
144  $message = htmlspecialchars($message);
145  $message = sprintf($message, '<strong>', '</strong><br>', $templateFile);
146  }
147  } else {
148  $message = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutSuccess');
149  $message = htmlspecialchars($message);
150  $message = sprintf($message, '<a href="' . htmlspecialchars($returnUrl) . '">', '</a>');
151  }
152  return sprintf($message, htmlspecialchars($returnUrl));
153  }
154 
171  protected function ‪getPreviewConfigurationFromRequest(ServerRequestInterface $request, string $inputCode): ?array
172  {
173  $previewData = $this->‪getPreviewData($inputCode);
174  if (!is_array($previewData)) {
175  // ADMCMD command could not be executed! (No keyword configuration found)
176  return null;
177  }
178  if ($request->getMethod() === 'POST') {
179  throw new \Exception('POST requests are incompatible with keyword preview.', 1294585191);
180  }
181  // Validate configuration
182  $previewConfig = json_decode($previewData['config'], true);
183  if (!$previewConfig['fullWorkspace']) {
184  throw new \Exception('Preview configuration did not include a workspace preview', 1294585190);
185  }
186  // If the GET parameter ADMCMD_prev is set, then a cookie is set for the next request
187  if ($request->getQueryParams()[$this->previewKey] ?? false) {
188  $this->‪setCookie($inputCode, $request->getAttribute('normalizedParams'));
189  }
190  return $previewConfig;
191  }
192 
200  protected function ‪initializePreviewUser(int $workspaceUid, $requestedPageId)
201  {
202  if ($workspaceUid > 0) {
203  $previewUser = GeneralUtility::makeInstance(PreviewUserAuthentication::class);
204  $previewUser->setWebmounts([$requestedPageId]);
205  if ($previewUser->setTemporaryWorkspace($workspaceUid)) {
206  return $previewUser;
207  }
208  }
209  return false;
210  }
211 
218  protected function ‪setCookie(string $inputCode, ‪NormalizedParams $normalizedParams)
219  {
220  $cookieSameSite = $this->sanitizeSameSiteCookieValue(
221  strtolower(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieSameSite'] ?? Cookie::SAMESITE_STRICT)
222  );
223  // None needs the secure option (only allowed on HTTPS)
224  $cookieSecure = $cookieSameSite === Cookie::SAMESITE_NONE || $normalizedParams->‪isHttps();
225 
226  $cookie = new Cookie(
227  $this->previewKey,
228  $inputCode,
229  0,
230  $normalizedParams->‪getSitePath(),
231  null,
232  $cookieSecure,
233  true,
234  false,
235  $cookieSameSite
236  );
237  header('Set-Cookie: ' . $cookie->__toString(), false);
238  }
239 
247  protected function ‪getPreviewInputCode(ServerRequestInterface $request): string
248  {
249  return $request->getQueryParams()[‪$this->previewKey] ?? $request->getCookieParams()[‪$this->previewKey] ?? '';
250  }
251 
258  protected function ‪getPreviewData(string $keyword)
259  {
260  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
261  ->getQueryBuilderForTable('sys_preview');
262  return $queryBuilder
263  ->select('*')
264  ->from('sys_preview')
265  ->where(
266  $queryBuilder->expr()->eq(
267  'keyword',
268  $queryBuilder->createNamedParameter($keyword)
269  ),
270  $queryBuilder->expr()->gt(
271  'endtime',
272  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
273  )
274  )
275  ->setMaxResults(1)
276  ->execute()
277  ->fetch();
278  }
279 
299  protected function ‪renderPreviewInfo(‪TypoScriptFrontendController $tsfe, ‪NormalizedParams $normalizedParams): string
300  {
301  $content = '';
302  if (!isset($tsfe->config['config']['disablePreviewNotification']) || (int)$tsfe->config['config']['disablePreviewNotification'] !== 1) {
303  // get the title of the current workspace
304  $currentWorkspaceId = $tsfe->‪whichWorkspace();
305  $currentWorkspaceTitle = $this->‪getWorkspaceTitle($currentWorkspaceId);
306  $currentWorkspaceTitle = htmlspecialchars($currentWorkspaceTitle);
307  if ($tsfe->config['config']['message_preview_workspace']) {
308  $content = sprintf(
309  $tsfe->config['config']['message_preview_workspace'],
310  $currentWorkspaceTitle,
311  $currentWorkspaceId ?? -99
312  );
313  } else {
314  $text = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewText');
315  $text = htmlspecialchars($text);
316  $text = sprintf($text, $currentWorkspaceTitle, $currentWorkspaceId ?? -99);
317  $stopPreviewText = $this->‪getLanguageService()->‪sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stopPreview');
318  $stopPreviewText = htmlspecialchars($stopPreviewText);
319  if (‪$GLOBALS['BE_USER'] instanceof ‪PreviewUserAuthentication) {
320  $url = $this->‪removePreviewParameterFromUrl($normalizedParams->‪getRequestUri());
321  $urlForStoppingPreview = $normalizedParams->‪getSiteUrl() . 'index.php?returnUrl=' . rawurlencode($url) . '&ADMCMD_prev=LOGOUT';
322  $text .= '<br><a style="color: #000; pointer-events: visible;" href="' . htmlspecialchars($urlForStoppingPreview) . '">' . $stopPreviewText . '</a>';
323  }
324  $styles = [];
325  $styles[] = 'position: fixed';
326  $styles[] = 'top: 15px';
327  $styles[] = 'right: 15px';
328  $styles[] = 'padding: 8px 18px';
329  $styles[] = 'background: #fff3cd';
330  $styles[] = 'border: 1px solid #ffeeba';
331  $styles[] = 'font-family: sans-serif';
332  $styles[] = 'font-size: 14px';
333  $styles[] = 'font-weight: bold';
334  $styles[] = 'color: #856404';
335  $styles[] = 'z-index: 20000';
336  $styles[] = 'user-select: none';
337  $styles[] = 'pointer-events: none';
338  $styles[] = 'text-align: center';
339  $styles[] = 'border-radius: 2px';
340  $content = '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . $text . '</div>';
341  }
342  }
343  return $content;
344  }
345 
352  protected function ‪getWorkspaceTitle(int $workspaceId): string
353  {
354  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
355  ->getQueryBuilderForTable('sys_workspace');
356  $title = $queryBuilder
357  ->select('title')
358  ->from('sys_workspace')
359  ->where(
360  $queryBuilder->expr()->eq(
361  'uid',
362  $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
363  )
364  )
365  ->execute()
366  ->fetchColumn();
367  return (string)($title !== false ? $title : '');
368  }
369 
376  protected function ‪removePreviewParameterFromUrl(string $url): string
377  {
378  return (string)preg_replace('/\\&?' . $this->previewKey . '=[[:alnum:]]+/', '', $url);
379  }
380 
384  protected function ‪getLanguageService(): ‪LanguageService
385  {
386  return ‪$GLOBALS['LANG'] ?: GeneralUtility::makeInstance(LanguageService::class);
387  }
388 
395  protected function ‪setBackendUserAspect(‪Context $context, ‪BackendUserAuthentication $user = null)
396  {
397  $context->‪setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class, $user));
398  $context->‪setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $user ? $user->workspace : 0));
399  }
400 }
‪TYPO3\CMS\Core\Context\WorkspaceAspect
Definition: WorkspaceAspect.php:29
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewData
‪mixed getPreviewData(string $keyword)
Definition: WorkspacePreview.php:257
‪TYPO3\CMS\Core\Http\NormalizedParams\getSitePath
‪string getSitePath()
Definition: NormalizedParams.php:422
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\setBackendUserAspect
‪setBackendUserAspect(Context $context, BackendUserAuthentication $user=null)
Definition: WorkspacePreview.php:394
‪TYPO3\CMS\Workspaces\Middleware
Definition: WorkspacePreview.php:3
‪TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication
Definition: PreviewUserAuthentication.php:44
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLogoutTemplateMessage
‪string getLogoutTemplateMessage(string $returnUrl='')
Definition: WorkspacePreview.php:133
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewConfigurationFromRequest
‪array null getPreviewConfigurationFromRequest(ServerRequestInterface $request, string $inputCode)
Definition: WorkspacePreview.php:170
‪TYPO3\CMS\Core\Localization\LanguageService\sL
‪string sL($input)
Definition: LanguageService.php:158
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:49
‪TYPO3\CMS\Core\Http\NormalizedParams\getSiteUrl
‪string getSiteUrl()
Definition: NormalizedParams.php:414
‪TYPO3\CMS\Core\Http\NormalizedParams\getRequestUri
‪string getRequestUri()
Definition: NormalizedParams.php:350
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\setCookie
‪setCookie(string $inputCode, NormalizedParams $normalizedParams)
Definition: WorkspacePreview.php:217
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:141
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\$previewKey
‪string $previewKey
Definition: WorkspacePreview.php:53
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\initializePreviewUser
‪PreviewUserAuthentication bool initializePreviewUser(int $workspaceUid, $requestedPageId)
Definition: WorkspacePreview.php:199
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getWorkspaceTitle
‪string getWorkspaceTitle(int $workspaceId)
Definition: WorkspacePreview.php:351
‪TYPO3\CMS\Core\Http\Stream
Definition: Stream.php:28
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:45
‪TYPO3\CMS\Core\Http\NormalizedParams\isHttps
‪bool isHttps()
Definition: NormalizedParams.php:310
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getPreviewInputCode
‪string getPreviewInputCode(ServerRequestInterface $request)
Definition: WorkspacePreview.php:246
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview
Definition: WorkspacePreview.php:46
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\removePreviewParameterFromUrl
‪string removePreviewParameterFromUrl(string $url)
Definition: WorkspacePreview.php:375
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\getLanguageService
‪LanguageService getLanguageService()
Definition: WorkspacePreview.php:383
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController\whichWorkspace
‪int whichWorkspace()
Definition: TypoScriptFrontendController.php:4256
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\renderPreviewInfo
‪string renderPreviewInfo(TypoScriptFrontendController $tsfe, NormalizedParams $normalizedParams)
Definition: WorkspacePreview.php:298
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:97
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:29
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Workspaces\Middleware\WorkspacePreview\process
‪ResponseInterface process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: WorkspacePreview.php:68
‪TYPO3\CMS\Core\Context\UserAspect
Definition: UserAspect.php:36
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:32
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:25