[TASK] Use PageRenderer directly in LoginController
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / LoginController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Controller;
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\Log\LoggerAwareInterface;
21 use Psr\Log\LoggerAwareTrait;
22 use TYPO3\CMS\Backend\Exception;
23 use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
24 use TYPO3\CMS\Backend\Routing\UriBuilder;
25 use TYPO3\CMS\Backend\Template\DocumentTemplate;
26 use TYPO3\CMS\Backend\Utility\BackendUtility;
27 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
28 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
29 use TYPO3\CMS\Core\Database\ConnectionPool;
30 use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
31 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
32 use TYPO3\CMS\Core\Http\HtmlResponse;
33 use TYPO3\CMS\Core\Localization\LanguageService;
34 use TYPO3\CMS\Core\Localization\Locales;
35 use TYPO3\CMS\Core\Page\PageRenderer;
36 use TYPO3\CMS\Core\Utility\GeneralUtility;
37 use TYPO3\CMS\Core\Utility\HttpUtility;
38 use TYPO3\CMS\Core\Utility\PathUtility;
39 use TYPO3\CMS\Fluid\View\StandaloneView;
40
41 /**
42 * Script Class for rendering the login form
43 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
44 */
45 class LoginController implements LoggerAwareInterface
46 {
47 use LoggerAwareTrait;
48
49 /**
50 * The URL to redirect to after login.
51 *
52 * @var string
53 */
54 protected $redirectUrl;
55
56 /**
57 * Set to the redirect URL of the form (may be redirect_url or "index.php?M=main")
58 *
59 * @var string
60 */
61 protected $redirectToURL;
62
63 /**
64 * the active login provider identifier
65 *
66 * @var string
67 */
68 protected $loginProviderIdentifier;
69
70 /**
71 * List of registered and sorted login providers
72 *
73 * @var array
74 */
75 protected $loginProviders = [];
76
77 /**
78 * Login-refresh bool; The backend will call this script
79 * with this value set when the login is close to being expired
80 * and the form needs to be redrawn.
81 *
82 * @var bool
83 */
84 protected $loginRefresh;
85
86 /**
87 * Value of forms submit button for login.
88 *
89 * @var string
90 */
91 protected $submitValue;
92
93 /**
94 * @var StandaloneView
95 */
96 protected $view;
97
98 /**
99 * @var DocumentTemplate
100 */
101 protected $documentTemplate;
102
103 /**
104 * Injects the request and response objects for the current request or subrequest
105 * As this controller goes only through the main() method, it is rather simple for now
106 *
107 * @param ServerRequestInterface $request the current request
108 * @return ResponseInterface the finished response with the content
109 */
110 public function formAction(ServerRequestInterface $request): ResponseInterface
111 {
112 $this->init($request);
113 return new HtmlResponse($this->createLoginLogoutForm($request));
114 }
115
116 /**
117 * Calls the main function but with loginRefresh enabled at any time
118 *
119 * @param ServerRequestInterface $request the current request
120 * @return ResponseInterface the finished response with the content
121 */
122 public function refreshAction(ServerRequestInterface $request): ResponseInterface
123 {
124 $this->init($request);
125 $this->loginRefresh = true;
126 return new HtmlResponse($this->createLoginLogoutForm($request));
127 }
128
129 /**
130 * This can be called by single login providers, they receive an instance of $this
131 *
132 * @return string
133 */
134 public function getLoginProviderIdentifier()
135 {
136 return $this->loginProviderIdentifier;
137 }
138
139 /**
140 * Initialize the login box. Will also react on a &L=OUT flag and exit.
141 *
142 * @param ServerRequestInterface $request the current request
143 */
144 protected function init(ServerRequestInterface $request): void
145 {
146 $this->documentTemplate = GeneralUtility::makeInstance(DocumentTemplate::class);
147 $parsedBody = $request->getParsedBody();
148 $queryParams = $request->getQueryParams();
149 $this->validateAndSortLoginProviders();
150
151 // We need a PHP session session for most login levels
152 session_start();
153 $this->redirectUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['redirect_url'] ?? $queryParams['redirect_url'] ?? null);
154 $this->loginProviderIdentifier = $this->detectLoginProvider($request);
155
156 $this->loginRefresh = (bool)($parsedBody['loginRefresh'] ?? $queryParams['loginRefresh'] ?? false);
157 // Value of "Login" button. If set, the login button was pressed.
158 $this->submitValue = $parsedBody['commandLI'] ?? $queryParams['commandLI'] ?? null;
159 // Try to get the preferred browser language
160 /** @var Locales $locales */
161 $locales = GeneralUtility::makeInstance(Locales::class);
162 $httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'];
163 $preferredBrowserLanguage = $locales
164 ->getPreferredClientLanguage($httpAcceptLanguage);
165
166 // If we found a $preferredBrowserLanguage and it is not the default language and no be_user is logged in
167 // initialize $this->getLanguageService() again with $preferredBrowserLanguage
168 if ($preferredBrowserLanguage !== 'default' && empty($this->getBackendUserAuthentication()->user['uid'])) {
169 $this->getLanguageService()->init($preferredBrowserLanguage);
170 GeneralUtility::makeInstance(PageRenderer::class)->setLanguage($preferredBrowserLanguage);
171 }
172
173 $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_login.xlf');
174
175 // Setting the redirect URL to "index.php?M=main" if no alternative input is given
176 if ($this->redirectUrl) {
177 $this->redirectToURL = $this->redirectUrl;
178 } else {
179 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
180 $this->redirectToURL = (string)$uriBuilder->buildUriFromRoute('main');
181 }
182
183 // If "L" is "OUT", then any logged in is logged out. If redirect_url is given, we redirect to it
184 if (($parsedBody['L'] ?? $queryParams['L'] ?? null) === 'OUT' && is_object($this->getBackendUserAuthentication())) {
185 $this->getBackendUserAuthentication()->logoff();
186 $this->redirectToUrl();
187 }
188
189 $this->view = $this->getFluidTemplateObject();
190 }
191
192 /**
193 * Main function - creating the login/logout form
194 *
195 * @param ServerRequestInterface $request
196 * @return string $content
197 * @throws Exception
198 */
199 protected function createLoginLogoutForm(ServerRequestInterface $request): string
200 {
201 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
202 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Login');
203
204 // Checking, if we should make a redirect.
205 // Might set JavaScript in the header to close window.
206 $this->checkRedirect($request, $pageRenderer);
207
208 // Extension Configuration
209 $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
210
211 // Background Image
212 if (!empty($extConf['loginBackgroundImage'])) {
213 $backgroundImage = $this->getUriForFileName($extConf['loginBackgroundImage']);
214 if ($backgroundImage === '') {
215 $this->logger->warning(
216 'The configured TYPO3 backend login background image "' . htmlspecialchars($extConf['loginBackgroundImage']) .
217 '" can\'t be resolved. Please check if the file exists and the extension is activated.'
218 );
219 }
220 $pageRenderer->addCssInlineBlock('loginBackgroundImage', '
221 .typo3-login-carousel-control.right,
222 .typo3-login-carousel-control.left,
223 .panel-login { border: 0; }
224 .typo3-login { background-image: url("' . $backgroundImage . '"); }
225 .typo3-login-footnote { background-color: #000000; color: #ffffff; opacity: 0.5; }
226 ');
227 }
228
229 // Login Footnote
230 if (!empty($extConf['loginFootnote'])) {
231 $this->view->assign('loginFootnote', strip_tags(trim($extConf['loginFootnote'])));
232 }
233
234 // Add additional css to use the highlight color in the login screen
235 if (!empty($extConf['loginHighlightColor'])) {
236 $pageRenderer->addCssInlineBlock('loginHighlightColor', '
237 .btn-login.disabled, .btn-login[disabled], fieldset[disabled] .btn-login,
238 .btn-login.disabled:hover, .btn-login[disabled]:hover, fieldset[disabled] .btn-login:hover,
239 .btn-login.disabled:focus, .btn-login[disabled]:focus, fieldset[disabled] .btn-login:focus,
240 .btn-login.disabled.focus, .btn-login[disabled].focus, fieldset[disabled] .btn-login.focus,
241 .btn-login.disabled:active, .btn-login[disabled]:active, fieldset[disabled] .btn-login:active,
242 .btn-login.disabled.active, .btn-login[disabled].active, fieldset[disabled] .btn-login.active,
243 .btn-login:hover, .btn-login:focus, .btn-login:active,
244 .btn-login:active:hover, .btn-login:active:focus,
245 .btn-login { background-color: ' . $extConf['loginHighlightColor'] . '; }
246 .panel-login .panel-body { border-color: ' . $extConf['loginHighlightColor'] . '; }
247 ');
248 }
249
250 // Logo
251 if (!empty($extConf['loginLogo'])) {
252 if ($this->getUriForFileName($extConf['loginLogo']) === '') {
253 $this->logger->warning(
254 'The configured TYPO3 backend login logo "' . htmlspecialchars($extConf['loginLogo']) .
255 '" can\'t be resolved. Please check if the file exists and the extension is activated.'
256 );
257 }
258 $logo = $extConf['loginLogo'];
259 } else {
260 // Use TYPO3 logo depending on highlight color
261 if (!empty($extConf['loginHighlightColor'])) {
262 $logo = 'EXT:backend/Resources/Public/Images/typo3_black.svg';
263 } else {
264 $logo = 'EXT:backend/Resources/Public/Images/typo3_orange.svg';
265 }
266 $pageRenderer->addCssInlineBlock('loginLogo', '
267 .typo3-login-logo .typo3-login-image { max-width: 150px; height:100%;}
268 ');
269 }
270 $logo = $this->getUriForFileName($logo);
271
272 // Start form
273 $formType = empty($this->getBackendUserAuthentication()->user['uid']) ? 'LoginForm' : 'LogoutForm';
274 $this->view->assignMultiple([
275 'backendUser' => $this->getBackendUserAuthentication()->user,
276 'hasLoginError' => $this->isLoginInProgress($request),
277 'formType' => $formType,
278 'logo' => $logo,
279 'images' => [
280 'capslock' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/icon_capslock.svg'),
281 'typo3' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/typo3_orange.svg'),
282 ],
283 'copyright' => BackendUtility::TYPO3_copyRightNotice(),
284 'redirectUrl' => $this->redirectUrl,
285 'loginRefresh' => $this->loginRefresh,
286 'loginNewsItems' => $this->getSystemNews(),
287 'loginProviderIdentifier' => $this->loginProviderIdentifier,
288 'loginProviders' => $this->loginProviders
289 ]);
290
291 // Initialize interface selectors:
292 $this->makeInterfaceSelector($request);
293
294 /** @var LoginProviderInterface $loginProvider */
295 $loginProvider = GeneralUtility::makeInstance($this->loginProviders[$this->loginProviderIdentifier]['provider']);
296 $loginProvider->render($this->view, $pageRenderer, $this);
297
298 $content = $this->documentTemplate->startPage('TYPO3 CMS Login: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
299 $content .= $this->view->render();
300 $content .= $this->documentTemplate->endPage();
301
302 return $content;
303 }
304
305 /**
306 * Checking, if we should perform some sort of redirection OR closing of windows.
307 *
308 * Do a redirect if a user is logged in
309 *
310 * @param ServerRequestInterface $request
311 * @param PageRenderer $pageRenderer
312 * @throws \RuntimeException
313 * @throws \UnexpectedValueException
314 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
315 */
316 protected function checkRedirect(ServerRequestInterface $request, PageRenderer $pageRenderer): void
317 {
318 $backendUser = $this->getBackendUserAuthentication();
319 if (empty($backendUser->user['uid'])) {
320 return;
321 }
322
323 /*
324 * If no cookie has been set previously, we tell people that this is a problem.
325 * This assumes that a cookie-setting script (like this one) has been hit at
326 * least once prior to this instance.
327 */
328 if (!isset($_COOKIE[BackendUserAuthentication::getCookieName()])) {
329 if ($this->submitValue === 'setCookie') {
330 /*
331 * we tried it a second time but still no cookie
332 * 26/4 2005: This does not work anymore, because the saving of challenge values
333 * in $_SESSION means the system will act as if the password was wrong.
334 */
335 throw new \RuntimeException('Login-error: Yeah, that\'s a classic. No cookies, no TYPO3. ' .
336 'Please accept cookies from TYPO3 - otherwise you\'ll not be able to use the system.', 1294586846);
337 }
338 // try it once again - that might be needed for auto login
339 $this->redirectToURL = 'index.php?commandLI=setCookie';
340 }
341 $redirectToUrl = (string)($backendUser->getTSConfig()['auth.']['BE.']['redirectToURL'] ?? '');
342 if (empty($redirectToUrl)) {
343 // Based on the interface we set the redirect script
344 $parsedBody = $request->getParsedBody();
345 $queryParams = $request->getQueryParams();
346 $interface = $parsedBody['interface'] ?? $queryParams['interface'] ?? '';
347 switch ($interface) {
348 case 'frontend':
349 $this->redirectToURL = '../';
350 break;
351 case 'backend':
352 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
353 $this->redirectToURL = (string)$uriBuilder->buildUriFromRoute('main');
354 break;
355 }
356 } else {
357 $this->redirectToURL = $redirectToUrl;
358 $interface = '';
359 }
360 // store interface
361 $backendUser->uc['interfaceSetup'] = $interface;
362 $backendUser->writeUC();
363
364 $formProtection = FormProtectionFactory::get();
365 if (!$formProtection instanceof BackendFormProtection) {
366 throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411);
367 }
368 if ($this->loginRefresh) {
369 $formProtection->setSessionTokenFromRegistry();
370 $formProtection->persistSessionToken();
371 $pageRenderer->addJsInlineCode('loginRefresh', '
372 if (window.opener && window.opener.TYPO3 && window.opener.TYPO3.LoginRefresh) {
373 window.opener.TYPO3.LoginRefresh.startTask();
374 window.close();
375 }
376 ');
377 } else {
378 $formProtection->storeSessionTokenInRegistry();
379 $this->redirectToUrl();
380 }
381 }
382
383 /**
384 * Making interface selector
385 * @param ServerRequestInterface $request
386 */
387 protected function makeInterfaceSelector(ServerRequestInterface $request): void
388 {
389 // If interfaces are defined AND no input redirect URL in GET vars:
390 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['interfaces'] && ($this->isLoginInProgress($request) || !$this->redirectUrl)) {
391 $parts = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['interfaces']);
392 if (count($parts) > 1) {
393 // Only if more than one interface is defined we will show the selector
394 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
395 $interfaces = [
396 'backend' => [
397 'label' => $this->getLanguageService()->getLL('interface.backend'),
398 'jumpScript' => (string)$uriBuilder->buildUriFromRoute('main'),
399 'interface' => 'backend'
400 ],
401 'frontend' => [
402 'label' => $this->getLanguageService()->getLL('interface.frontend'),
403 'jumpScript' => '../',
404 'interface' => 'frontend'
405 ]
406 ];
407
408 $this->view->assign('showInterfaceSelector', true);
409 $this->view->assign('interfaces', $interfaces);
410 } elseif (!$this->redirectUrl) {
411 // If there is only ONE interface value set and no redirect_url is present
412 $this->view->assign('showInterfaceSelector', false);
413 $this->view->assign('interface', $parts[0]);
414 }
415 }
416 }
417
418 /**
419 * Gets news from sys_news and converts them into a format suitable for
420 * showing them at the login screen.
421 *
422 * @return array An array of login news.
423 */
424 protected function getSystemNews(): array
425 {
426 $systemNewsTable = 'sys_news';
427 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
428 ->getQueryBuilderForTable($systemNewsTable);
429 $systemNews = [];
430 $systemNewsRecords = $queryBuilder
431 ->select('title', 'content', 'crdate')
432 ->from($systemNewsTable)
433 ->orderBy('crdate', 'DESC')
434 ->execute()
435 ->fetchAll();
436 foreach ($systemNewsRecords as $systemNewsRecord) {
437 $systemNews[] = [
438 'date' => date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$systemNewsRecord['crdate']),
439 'header' => $systemNewsRecord['title'],
440 'content' => $systemNewsRecord['content']
441 ];
442 }
443 return $systemNews;
444 }
445
446 /**
447 * Returns the uri of a relative reference, resolves the "EXT:" prefix
448 * (way of referring to files inside extensions) and checks that the file is inside
449 * the project root of the TYPO3 installation
450 *
451 * @param string $filename The input filename/filepath to evaluate
452 * @return string Returns the filename of $filename if valid, otherwise blank string.
453 * @internal
454 */
455 private function getUriForFileName($filename): string
456 {
457 // Check if it's already a URL
458 if (preg_match('/^(https?:)?\/\//', $filename)) {
459 return $filename;
460 }
461 $absoluteFilename = GeneralUtility::getFileAbsFileName(ltrim($filename, '/'));
462 $filename = '';
463 if ($absoluteFilename !== '' && @is_file($absoluteFilename)) {
464 $filename = PathUtility::getAbsoluteWebPath($absoluteFilename);
465 }
466 return $filename;
467 }
468
469 /**
470 * Checks if login credentials are currently submitted
471 *
472 * @param ServerRequestInterface $request
473 * @return bool
474 */
475 protected function isLoginInProgress(ServerRequestInterface $request): bool
476 {
477 $parsedBody = $request->getParsedBody();
478 $queryParams = $request->getQueryParams();
479 $username = $parsedBody['username'] ?? $queryParams['username'] ?? null;
480 return !empty($username) || !empty($this->submitValue);
481 }
482
483 /**
484 * returns a new standalone view, shorthand function
485 *
486 * @return StandaloneView
487 */
488 protected function getFluidTemplateObject()
489 {
490 /** @var StandaloneView $view */
491 $view = GeneralUtility::makeInstance(StandaloneView::class);
492 $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
493 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
494 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
495
496 $view->getRequest()->setControllerExtensionName('Backend');
497 return $view;
498 }
499
500 /**
501 * Wrapper method to redirect to configured redirect URL
502 */
503 protected function redirectToUrl(): void
504 {
505 HttpUtility::redirect($this->redirectToURL);
506 }
507
508 /**
509 * Validates the registered login providers
510 *
511 * @throws \RuntimeException
512 */
513 protected function validateAndSortLoginProviders()
514 {
515 $providers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'] ?? [];
516 if (empty($providers) || !is_array($providers)) {
517 throw new \RuntimeException('No login providers are registered in $GLOBALS[\'TYPO3_CONF_VARS\'][\'EXTCONF\'][\'backend\'][\'loginProviders\'].', 1433417281);
518 }
519 foreach ($providers as $identifier => $configuration) {
520 if (empty($configuration) || !is_array($configuration)) {
521 throw new \RuntimeException('Missing configuration for login provider "' . $identifier . '".', 1433416043);
522 }
523 if (!is_string($configuration['provider']) || empty($configuration['provider']) || !class_exists($configuration['provider']) || !is_subclass_of($configuration['provider'], LoginProviderInterface::class)) {
524 throw new \RuntimeException('The login provider "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . LoginProviderInterface::class . '".', 1460977275);
525 }
526 if (empty($configuration['label'])) {
527 throw new \RuntimeException('Missing label definition for login provider "' . $identifier . '".', 1433416044);
528 }
529 if (empty($configuration['icon-class'])) {
530 throw new \RuntimeException('Missing icon definition for login provider "' . $identifier . '".', 1433416045);
531 }
532 if (!isset($configuration['sorting'])) {
533 throw new \RuntimeException('Missing sorting definition for login provider "' . $identifier . '".', 1433416046);
534 }
535 }
536 // sort providers
537 uasort($providers, function ($a, $b) {
538 return $b['sorting'] - $a['sorting'];
539 });
540 $this->loginProviders = $providers;
541 }
542
543 /**
544 * Detect the login provider, get from request or choose the
545 * first one as default
546 *
547 * @param ServerRequestInterface $request
548 * @return string
549 */
550 protected function detectLoginProvider(ServerRequestInterface $request): string
551 {
552 $parsedBody = $request->getParsedBody();
553 $queryParams = $request->getQueryParams();
554 $loginProvider = $parsedBody['loginProvider'] ?? $queryParams['loginProvider'] ?? '';
555 if ((empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) && !empty($_COOKIE['be_lastLoginProvider'])) {
556 $loginProvider = $_COOKIE['be_lastLoginProvider'];
557 }
558 if (empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) {
559 reset($this->loginProviders);
560 $loginProvider = key($this->loginProviders);
561 }
562 // Use the secure option when the current request is served by a secure connection:
563 $normalizedParams = $request->getAttribute('normalizedParams');
564 $isHttps = $normalizedParams->isHttps();
565 $cookieSecure = (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieSecure'] && $isHttps;
566 setcookie('be_lastLoginProvider', (string)$loginProvider, $GLOBALS['EXEC_TIME'] + 7776000, '', '', $cookieSecure, true); // 90 days
567 return (string)$loginProvider;
568 }
569
570 /**
571 * Returns LanguageService
572 *
573 * @return LanguageService
574 */
575 protected function getLanguageService(): LanguageService
576 {
577 return $GLOBALS['LANG'];
578 }
579
580 /**
581 * @return BackendUserAuthentication
582 */
583 protected function getBackendUserAuthentication(): BackendUserAuthentication
584 {
585 return $GLOBALS['BE_USER'];
586 }
587 }