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