[!!!][TASK] Drop "documentation" extension
[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\Localization\LanguageService;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
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 $returnUrl = GeneralUtility::sanitizeLocalUrl($returnUrl);
125 $returnUrl = $this->removePreviewParameterFromUrl($returnUrl);
126 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
127 $templateFile = GeneralUtility::getFileAbsFileName($GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']);
128 if (@is_file($templateFile)) {
129 $message = file_get_contents($templateFile);
130 } else {
131 $message = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutError');
132 $message = htmlspecialchars($message);
133 $message = sprintf($message, '<strong>', '</strong><br>', $templateFile);
134 }
135 } else {
136 $message = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewLogoutSuccess');
137 $message = htmlspecialchars($message);
138 $message = sprintf($message, '<a href="' . htmlspecialchars($returnUrl) . '">', '</a>');
139 }
140 return sprintf($message, htmlspecialchars($returnUrl));
141 }
142
143 /**
144 * Looking for an ADMCMD_prev code, looks it up if found and returns configuration data.
145 * Background: From the backend a request to the frontend to show a page, possibly with
146 * workspace preview can be "recorded" and associated with a keyword.
147 * When the frontend is requested with this keyword the associated request parameters are
148 * restored from the database AND the backend user is loaded - only for that request.
149 * The main point is that a special URL valid for a limited time,
150 * eg. http://localhost/typo3site/index.php?ADMCMD_prev=035d9bf938bd23cb657735f68a8cedbf will
151 * open up for a preview that doesn't require login. Thus it's useful for sending in an email
152 * to someone without backend account.
153 *
154 * @param ServerRequestInterface $request
155 * @param string $inputCode
156 * @return array Preview configuration array from sys_preview record.
157 * @throws \Exception
158 */
159 protected function getPreviewConfigurationFromRequest(ServerRequestInterface $request, string $inputCode): array
160 {
161 $previewData = $this->getPreviewData($inputCode);
162 if (!is_array($previewData)) {
163 throw new \Exception('ADMCMD command could not be executed! (No keyword configuration found)', 1294585192);
164 }
165 if ($request->getMethod() === 'POST') {
166 throw new \Exception('POST requests are incompatible with keyword preview.', 1294585191);
167 }
168 // Validate configuration
169 $previewConfig = json_decode($previewData['config'], true);
170 if (!$previewConfig['fullWorkspace']) {
171 throw new \Exception('Preview configuration did not include a workspace preview', 1294585190);
172 }
173 // If the GET parameter ADMCMD_prev is set, then a cookie is set for the next request
174 if ($request->getQueryParams()[$this->previewKey] ?? false) {
175 $this->setCookie($inputCode, $request->getAttribute('normalizedParams'));
176 }
177 return $previewConfig;
178 }
179
180 /**
181 * Creates a preview user and sets the workspace ID and the current page ID (for accessing the page)
182 *
183 * @param int $workspaceUid the workspace ID to set
184 * @param mixed $requestedPageId pageID or alias to the current page
185 * @return PreviewUserAuthentication|bool if the set up of the workspace was successful, the user is returned.
186 */
187 protected function initializePreviewUser(int $workspaceUid, $requestedPageId)
188 {
189 if ($workspaceUid > 0) {
190 $previewUser = GeneralUtility::makeInstance(PreviewUserAuthentication::class);
191 $previewUser->setWebmounts([$requestedPageId]);
192 if ($previewUser->setTemporaryWorkspace($workspaceUid)) {
193 return $previewUser;
194 }
195 }
196 return false;
197 }
198
199 /**
200 * Sets a cookie for logging in a preview user
201 *
202 * @param string $inputCode
203 * @param NormalizedParams $normalizedParams
204 */
205 protected function setCookie(string $inputCode, NormalizedParams $normalizedParams)
206 {
207 setcookie($this->previewKey, $inputCode, 0, $normalizedParams->getSitePath(), '', true, true);
208 }
209
210 /**
211 * Returns the input code value from the admin command variable
212 * If no inputcode and a cookie is set, load input code from cookie
213 *
214 * @param ServerRequestInterface $request
215 * @return string keyword
216 */
217 protected function getPreviewInputCode(ServerRequestInterface $request): string
218 {
219 return $request->getQueryParams()[$this->previewKey] ?? $request->getCookieParams()[$this->previewKey] ?? '';
220 }
221
222 /**
223 * Look for keyword configuration record in the database, but check if the keyword has expired already
224 *
225 * @param string $keyword
226 * @return mixed array of the result set or null
227 */
228 protected function getPreviewData(string $keyword)
229 {
230 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
231 ->getQueryBuilderForTable('sys_preview');
232 return $queryBuilder
233 ->select('*')
234 ->from('sys_preview')
235 ->where(
236 $queryBuilder->expr()->eq(
237 'keyword',
238 $queryBuilder->createNamedParameter($keyword)
239 ),
240 $queryBuilder->expr()->gt(
241 'endtime',
242 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
243 )
244 )
245 ->setMaxResults(1)
246 ->execute()
247 ->fetch();
248 }
249
250 /**
251 * Code regarding adding a custom preview message, when previewing a workspace
252 */
253
254 /**
255 * Renders a message at the bottom of the HTML page, can be modified via
256 *
257 * config.disablePreviewNotification = 1 (to disable the additional info text)
258 *
259 * and
260 *
261 * config.message_preview_workspace = This is not the online version but the version of "%s" workspace (ID: %s).
262 *
263 * via TypoScript.
264 *
265 * @param TypoScriptFrontendController $tsfe
266 * @param NormalizedParams $normalizedParams
267 * @return string
268 */
269 protected function renderPreviewInfo(TypoScriptFrontendController $tsfe, NormalizedParams $normalizedParams): string
270 {
271 if (!isset($tsfe->config['config']['disablePreviewNotification']) || (int)$tsfe->config['config']['disablePreviewNotification'] !== 1) {
272 // get the title of the current workspace
273 $currentWorkspaceId = $tsfe->whichWorkspace();
274 $currentWorkspaceTitle = $this->getWorkspaceTitle($currentWorkspaceId);
275 $currentWorkspaceTitle = htmlspecialchars($currentWorkspaceTitle);
276 if ($tsfe->config['config']['message_preview_workspace']) {
277 $content .= sprintf(
278 $tsfe->config['config']['message_preview_workspace'],
279 $currentWorkspaceTitle,
280 $currentWorkspaceId ?? -99
281 );
282 } else {
283 $text = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:previewText');
284 $text = htmlspecialchars($text);
285 $text = sprintf($text, $currentWorkspaceTitle, $currentWorkspaceId ?? -99);
286 $stopPreviewText = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:stopPreview');
287 $stopPreviewText = htmlspecialchars($stopPreviewText);
288 if ($GLOBALS['BE_USER'] instanceof PreviewUserAuthentication) {
289 $url = $this->removePreviewParameterFromUrl($normalizedParams->getRequestUri());
290 $urlForStoppingPreview = $normalizedParams->getSiteUrl() . 'index.php?returnUrl=' . rawurlencode($url) . '&ADMCMD_prev=LOGOUT';
291 $text .= '<br><a style="color: #000; pointer-events: visible;" href="' . htmlspecialchars($urlForStoppingPreview) . '">' . $stopPreviewText . '</a>';
292 }
293 $styles = [];
294 $styles[] = 'position: fixed';
295 $styles[] = 'top: 15px';
296 $styles[] = 'right: 15px';
297 $styles[] = 'padding: 8px 18px';
298 $styles[] = 'background: #fff3cd';
299 $styles[] = 'border: 1px solid #ffeeba';
300 $styles[] = 'font-family: sans-serif';
301 $styles[] = 'font-size: 14px';
302 $styles[] = 'font-weight: bold';
303 $styles[] = 'color: #856404';
304 $styles[] = 'z-index: 20000';
305 $styles[] = 'user-select: none';
306 $styles[] = 'pointer-events: none';
307 $styles[] = 'text-align: center';
308 $styles[] = 'border-radius: 2px';
309 $content .= '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . $text . '</div>';
310 }
311 }
312 return $content;
313 }
314
315 /**
316 * Fetches the title of the workspace
317 *
318 * @param $workspaceId
319 * @return string the title of the workspace
320 */
321 protected function getWorkspaceTitle(int $workspaceId): string
322 {
323 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
324 ->getQueryBuilderForTable('sys_workspace');
325 $title = $queryBuilder
326 ->select('title')
327 ->from('sys_workspace')
328 ->where(
329 $queryBuilder->expr()->eq(
330 'uid',
331 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT)
332 )
333 )
334 ->execute()
335 ->fetchColumn();
336 return $title !== false ? $title : '';
337 }
338
339 /**
340 * Used for generating URLs (e.g. in logout page) without the existing ADMCMD_prev keyword as GET variable
341 *
342 * @param string $url
343 * @return string
344 */
345 protected function removePreviewParameterFromUrl(string $url): string
346 {
347 return (string)preg_replace('/\\&?' . $this->previewKey . '=[[:alnum:]]+/', '', $url);
348 }
349
350 /**
351 * @return LanguageService
352 */
353 protected function getLanguageService(): LanguageService
354 {
355 return $GLOBALS['LANG'] ?: GeneralUtility::makeInstance(LanguageService::class);
356 }
357 }