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