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