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