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