dc436ed2c3d723d030dd9acf2cd41b99ac921b66
[Packages/TYPO3.CMS.git] / typo3 / sysext / workspaces / Classes / Middleware / WorkspacePreview.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Workspaces\Middleware;
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 TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Http\HtmlResponse;
24 use TYPO3\CMS\Core\Http\NormalizedParams;
25 use TYPO3\CMS\Core\Http\Stream;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
28 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
29 use TYPO3\CMS\Workspaces\Authentication\PreviewUserAuthentication;
30
31 /**
32 * Middleware to
33 * - evaluate ADMCMD_prev as GET parameter or from a cookie
34 * - initializes the PreviewUser as $GLOBALS['BE_USER']
35 * - renders a message about a possible workspace previewing currently
36 */
37 class WorkspacePreview implements MiddlewareInterface
38 {
39 /**
40 * The GET parameter to be used (also the cookie name)
41 *
42 * @var string
43 */
44 protected $previewKey = 'ADMCMD_prev';
45
46 /**
47 * Initializes a possible preview user (by checking for GET/cookie of name "ADMCMD_prev")
48 *
49 * The GET parameter "ADMCMD_noBeUser" can be used to preview a live workspace from the backend even if the
50 * backend user is in a different workspace.
51 *
52 * Additionally, if a workspace is previewed, an additional message text is shown.
53 *
54 * @param ServerRequestInterface $request
55 * @param RequestHandlerInterface $handler
56 * @return ResponseInterface
57 * @throws \Exception
58 */
59 public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
60 {
61 $keyword = $this->getPreviewInputCode($request);
62 if ($keyword) {
63 switch ($keyword) {
64 case 'IGNORE':
65 break;
66 case 'LOGOUT':
67 // "log out", and unset the cookie
68 $this->setCookie('', $request->getAttribute('normalizedParams'));
69 $message = $this->getLogoutTemplateMessage($request->getQueryParams()['returnUrl'] ?? '');
70 return new HtmlResponse($message);
71 default:
72 // A keyword was found in a query parameter or in a cookie
73 // If the keyword is valid, activate a BE User and override any existing BE Users
74 $configuration = $this->getPreviewConfigurationFromRequest($request, $keyword);
75 if (is_array($configuration) && $configuration['fullWorkspace'] > 0) {
76 $previewUser = $this->initializePreviewUser(
77 (int)$configuration['fullWorkspace'],
78 $GLOBALS['TSFE']->id
79 );
80 if ($previewUser) {
81 $GLOBALS['BE_USER'] = $previewUser;
82 $GLOBALS['TSFE']->beUserLogin = true;
83 }
84 }
85 }
86 }
87
88 // If "ADMCMD_noBeUser" is set, then ensure that there is no workspace preview and no BE User logged in.
89 // This option is solely used to ensure that a be user can preview the live version of a page in the
90 // workspace preview module.
91 if ($request->getQueryParams()['ADMCMD_noBeUser']) {
92 $GLOBALS['BE_USER'] = null;
93 $GLOBALS['TSFE']->beUserLogin = false;
94 // Caching is disabled, because otherwise generated URLs could include the ADMCMD_noBeUser parameter
95 $GLOBALS['TSFE']->set_no_cache('GET Parameter ADMCMD_noBeUser was given', true);
96 }
97
98 $response = $handler->handle($request);
99
100 // Add a info box to the frontend content
101 if ($GLOBALS['TSFE']->doWorkspacePreview() && $GLOBALS['TSFE']->isOutputting()) {
102 $previewInfo = $this->renderPreviewInfo($GLOBALS['TSFE'], $request->getAttribute('normalizedParams'));
103 $body = $response->getBody();
104 $body->rewind();
105 $content = $body->getContents();
106 $content = str_ireplace('</body>', $previewInfo . '</body>', $content);
107 $body = new Stream('php://temp', 'rw');
108 $body->write($content);
109 $response = $response->withBody($body);
110 }
111
112 return $response;
113 }
114
115 /**
116 * Renders the logout template when the "logout" button was pressed.
117 * Returns a string which can be put into a HttpResponse.
118 *
119 * @param string $returnUrl
120 * @return string
121 */
122 protected function getLogoutTemplateMessage(string $returnUrl = ''): string
123 {
124 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
125 $templateFile = GeneralUtility::getFileAbsFileName($GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']);
126 if (@is_file($templateFile)) {
127 $message = file_get_contents($templateFile);
128 } else {
129 $message = '<strong>ERROR!</strong><br>Template File "'
130 . $GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']
131 . '" configured with $TYPO3_CONF_VARS["FE"]["workspacePreviewLogoutTemplate"] not found. Please contact webmaster about this problem.';
132 }
133 } else {
134 $message = 'You logged out from Workspace preview mode. Click this link to <a href="%1$s">go back to the website</a>';
135 }
136 $returnUrl = GeneralUtility::sanitizeLocalUrl($returnUrl);
137 $returnUrl = $this->removePreviewParameterFromUrl($returnUrl);
138 return sprintf($message, htmlspecialchars($returnUrl));
139 }
140
141 /**
142 * Looking for an ADMCMD_prev code, looks it up if found and returns configuration data.
143 * Background: From the backend a request to the frontend to show a page, possibly with
144 * workspace preview can be "recorded" and associated with a keyword.
145 * When the frontend is requested with this keyword the associated request parameters are
146 * restored from the database AND the backend user is loaded - only for that request.
147 * The main point is that a special URL valid for a limited time,
148 * eg. http://localhost/typo3site/index.php?ADMCMD_prev=035d9bf938bd23cb657735f68a8cedbf will
149 * open up for a preview that doesn't require login. Thus it's useful for sending in an email
150 * to someone without backend account.
151 *
152 * @param ServerRequestInterface $request
153 * @param string $inputCode
154 * @return array Preview configuration array from sys_preview record.
155 * @throws \Exception
156 */
157 protected function getPreviewConfigurationFromRequest(ServerRequestInterface $request, string $inputCode)
158 {
159 $previewData = $this->getPreviewData($inputCode);
160 if (!is_array($previewData)) {
161 throw new \Exception('ADMCMD command could not be executed! (No keyword configuration found)', 1294585192);
162 }
163 if ($request->getMethod() === 'POST') {
164 throw new \Exception('POST requests are incompatible with keyword preview.', 1294585191);
165 }
166 // Validate configuration
167 $previewConfig = json_decode($previewData['config'], true);
168 if (!$previewConfig['fullWorkspace']) {
169 throw new \Exception('Preview configuration did not include a workspace preview', 1294585190);
170 }
171 // If the GET parameter ADMCMD_prev is set, then a cookie is set for the next request
172 if ($request->getQueryParams()[$this->previewKey] ?? false) {
173 $this->setCookie($inputCode, $request->getAttribute('normalizedParams'));
174 }
175 return $previewConfig;
176 }
177
178 /**
179 * Creates a preview user and sets the workspace ID and the current page ID (for accessing the page)
180 *
181 * @param int $workspaceUid the workspace ID to set
182 * @param mixed $requestedPageId pageID or alias to the current page
183 * @return PreviewUserAuthentication|bool if the set up of the workspace was successful, the user is returned.
184 */
185 protected function initializePreviewUser(int $workspaceUid, $requestedPageId)
186 {
187 if ($workspaceUid > 0) {
188 $previewUser = GeneralUtility::makeInstance(PreviewUserAuthentication::class);
189 $previewUser->setWebmounts([$requestedPageId]);
190 if ($previewUser->setTemporaryWorkspace($workspaceUid)) {
191 return $previewUser;
192 }
193 }
194 return false;
195 }
196
197 /**
198 * Sets a cookie for logging in a preview user
199 *
200 * @param string $inputCode
201 * @param NormalizedParams $normalizedParams
202 */
203 protected function setCookie(string $inputCode, NormalizedParams $normalizedParams)
204 {
205 setcookie($this->previewKey, $inputCode, 0, $normalizedParams->getSitePath(), '', true, true);
206 }
207
208 /**
209 * Returns the input code value from the admin command variable
210 * If no inputcode and a cookie is set, load input code from cookie
211 *
212 * @param ServerRequestInterface $request
213 * @return string keyword
214 */
215 protected function getPreviewInputCode(ServerRequestInterface $request): string
216 {
217 return $request->getQueryParams()[$this->previewKey] ?? $request->getCookieParams()[$this->previewKey] ?? '';
218 }
219
220 /**
221 * Look for keyword configuration record in the database, but check if the keyword has expired already
222 *
223 * @param string $keyword
224 * @return mixed array of the result set or null
225 */
226 protected function getPreviewData(string $keyword)
227 {
228 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
229 ->getQueryBuilderForTable('sys_preview');
230 return $queryBuilder
231 ->select('*')
232 ->from('sys_preview')
233 ->where(
234 $queryBuilder->expr()->eq(
235 'keyword',
236 $queryBuilder->createNamedParameter($keyword)
237 ),
238 $queryBuilder->expr()->gt(
239 'endtime',
240 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
241 )
242 )
243 ->setMaxResults(1)
244 ->execute()
245 ->fetch();
246 }
247
248 /**
249 * Code regarding adding a custom preview message, when previewing a workspace
250 */
251
252 /**
253 * Renders a message at the bottom of the HTML page, can be modified via
254 *
255 * config.disablePreviewNotification = 1 (to disable the additional info text)
256 *
257 * and
258 *
259 * config.message_preview_workspace = This is not the online version but the version of "%s" workspace (ID: %s).
260 *
261 * via TypoScript.
262 *
263 * @param TypoScriptFrontendController $tsfe
264 * @param NormalizedParams $normalizedParams
265 * @return string
266 */
267 protected function renderPreviewInfo(TypoScriptFrontendController $tsfe, NormalizedParams $normalizedParams): string
268 {
269 $backendDomain = $GLOBALS['BE_USER']->getSessionData('workspaces.backend_domain') ?: $normalizedParams->getRequestHostOnly();
270
271 $content = '<script type="text/javascript">
272 // having this is very important, otherwise the parent.resize call will fail
273 document.domain = ' . GeneralUtility::quoteJSvalue($backendDomain) . ';
274 </script>';
275
276 if (!isset($tsfe->config['config']['disablePreviewNotification']) || (int)$tsfe->config['config']['disablePreviewNotification'] !== 1) {
277 // get the title of the current workspace
278 $currentWorkspaceId = $tsfe->whichWorkspace();
279 $currentWorkspaceTitle = $this->getWorkspaceTitle($currentWorkspaceId);
280 $currentWorkspaceTitle = htmlspecialchars($currentWorkspaceTitle);
281 if ($tsfe->config['config']['message_preview_workspace']) {
282 $content .= sprintf(
283 $tsfe->config['config']['message_preview_workspace'],
284 $currentWorkspaceTitle,
285 $currentWorkspaceId ?? -99
286 );
287 } else {
288 $text = LocalizationUtility::translate(
289 'LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewText',
290 'workspaces',
291 [$currentWorkspaceTitle, $currentWorkspaceId ?? -99]
292 );
293 $text = htmlspecialchars($text);
294 if ($GLOBALS['BE_USER'] instanceof PreviewUserAuthentication) {
295 $url = $this->removePreviewParameterFromUrl($normalizedParams->getRequestUri());
296 $urlForStoppingPreview = $normalizedParams->getSiteUrl() . 'index.php?returnUrl=' . rawurlencode($url) . '&ADMCMD_prev=LOGOUT';
297 $text .= '<br><a style="color: #000; pointer-events: visible;" href="' . htmlspecialchars($urlForStoppingPreview) . '">Stop preview</a>';
298 }
299 $styles = [];
300 $styles[] = 'position: fixed';
301 $styles[] = 'top: 15px';
302 $styles[] = 'right: 15px';
303 $styles[] = 'padding: 8px 18px';
304 $styles[] = 'background: #fff3cd';
305 $styles[] = 'border: 1px solid #ffeeba';
306 $styles[] = 'font-family: sans-serif';
307 $styles[] = 'font-size: 14px';
308 $styles[] = 'font-weight: bold';
309 $styles[] = 'color: #856404';
310 $styles[] = 'z-index: 20000';
311 $styles[] = 'user-select: none';
312 $styles[] = 'pointer-events: none';
313 $styles[] = 'text-align: center';
314 $styles[] = 'border-radius: 2px';
315 $content .= '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . $text . '</div>';
316 }
317 }
318 return $content;
319 }
320
321 /**
322 * Fetches the title of the workspace
323 *
324 * @param $workspaceId
325 * @return string the title of the workspace
326 */
327 protected function getWorkspaceTitle(int $workspaceId): string
328 {
329 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
330 ->getQueryBuilderForTable('sys_workspace');
331 $title = $queryBuilder
332 ->select('title')
333 ->from('sys_workspace')
334 ->where(
335 $queryBuilder->expr()->eq(
336 'uid',
337 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
338 )
339 )
340 ->execute()
341 ->fetchColumn();
342 return $title !== false ? $title : '';
343 }
344
345 /**
346 * Used for generating URLs (e.g. in logout page) without the existing ADMCMD_prev keyword as GET variable
347 *
348 * @param string $url
349 * @return string
350 */
351 protected function removePreviewParameterFromUrl(string $url): string
352 {
353 return (string)preg_replace('/\\&?' . $this->previewKey . '=[[:alnum:]]+/', '', $url);
354 }
355 }