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