[FEATURE] Introduce Request/Response based on PSR-7
[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 TYPO3\CMS\Backend\Exception;
18 use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
22 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\HttpUtility;
25 use TYPO3\CMS\Core\Utility\PathUtility;
26 use TYPO3\CMS\Fluid\View\StandaloneView;
27
28 /**
29 * Script Class for rendering the login form
30 *
31 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
32 * @author Frank Nägler <typo3@naegler.net>
33 */
34 class LoginController implements \TYPO3\CMS\Core\Http\ControllerInterface {
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 "backend.php")
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 $this->validateAndSortLoginProviders();
90
91 // We need a PHP session session for most login levels
92 session_start();
93 $this->redirectUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('redirect_url'));
94 $this->loginProviderIdentifier = $this->detectLoginProvider();
95
96 $this->loginRefresh = (bool)GeneralUtility::_GP('loginRefresh');
97 // Value of "Login" button. If set, the login button was pressed.
98 $this->submitValue = GeneralUtility::_GP('commandLI');
99
100 // Try to get the preferred browser language
101 $preferredBrowserLanguage = $this->getLanguageService()->csConvObj
102 ->getPreferredClientLanguage(GeneralUtility::getIndpEnv('HTTP_ACCEPT_LANGUAGE'));
103
104 // If we found a $preferredBrowserLanguage and it is not the default language and no be_user is logged in
105 // initialize $this->getLanguageService() again with $preferredBrowserLanguage
106 if ($preferredBrowserLanguage !== 'default' && empty($this->getBackendUserAuthentication()->user['uid'])) {
107 $this->getLanguageService()->init($preferredBrowserLanguage);
108 }
109
110 $this->getLanguageService()->includeLLFile('EXT:lang/locallang_login.xlf');
111
112 // Setting the redirect URL to "backend.php" if no alternative input is given
113 $this->redirectToURL = $this->redirectUrl ?: 'backend.php';
114
115 // If "L" is "OUT", then any logged in is logged out. If redirect_url is given, we redirect to it
116 if (GeneralUtility::_GP('L') === 'OUT' && is_object($this->getBackendUserAuthentication())) {
117 $this->getBackendUserAuthentication()->logoff();
118 HttpUtility::redirect($this->redirectUrl);
119 }
120
121 $this->view = $this->getFluidTemplateObject();
122 }
123
124 /**
125 * Injects the request object for the current request or subrequest
126 * As this controller goes only through the main() method, it is rather simple for now
127 * This will be split up in an abstract controller once proper routing/dispatcher is in place.
128 *
129 * @param \Psr\Http\Message\RequestInterface $request
130 * @return \Psr\Http\Message\ResponseInterface $response
131 */
132 public function processRequest(\Psr\Http\Message\RequestInterface $request) {
133 $content = $this->main();
134 /** @var \TYPO3\CMS\Core\Http\Response $response */
135 $response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
136 $response->getBody()->write($content);
137 return $response;
138 }
139
140 /**
141 * Main function - creating the login/logout form
142 *
143 * @throws Exception
144 * @return string The content to output
145 */
146 public function main() {
147 /** @var $pageRenderer \TYPO3\CMS\Core\Page\PageRenderer */
148 $pageRenderer = $this->getDocumentTemplate()->getPageRenderer();
149 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Login');
150
151 // support placeholders for IE9 and lower
152 $clientInfo = GeneralUtility::clientInfo();
153 if ($clientInfo['BROWSER'] === 'msie' && $clientInfo['VERSION'] <= 9) {
154 $pageRenderer->addJsLibrary('placeholders', 'sysext/core/Resources/Public/JavaScript/Contrib/placeholders.jquery.min.js');
155 }
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']);
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 { 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 } elseif (!empty($GLOBALS['TBE_STYLES']['logo_login'])) {
196 // Fallback to old TBE_STYLES login logo
197 $logo = $GLOBALS['TBE_STYLES']['logo_login'];
198 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.');
199 } else {
200 // Use TYPO3 logo depending on highlight color
201 if (!empty($extConf['loginHighlightColor'])) {
202 $logo = 'EXT:backend/Resources/Public/Images/typo3_black.svg';
203 } else {
204 $logo = 'EXT:backend/Resources/Public/Images/typo3_orange.svg';
205 }
206 $this->getDocumentTemplate()->inDocStylesArray[] = '
207 .typo3-login-logo .typo3-login-image { max-width: 150px; }
208 ';
209 }
210 $logo = $this->getUriForFileName($logo);
211
212 // Start form
213 $formType = empty($this->getBackendUserAuthentication()->user['uid']) ? 'LoginForm' : 'LogoutForm';
214 $this->view->assignMultiple(array(
215 'backendUser' => $this->getBackendUserAuthentication()->user,
216 'hasLoginError' => $this->isLoginInProgress(),
217 'formType' => $formType,
218 'logo' => $logo,
219 'images' => array(
220 'capslock' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/icon_capslock.svg'),
221 'typo3' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/typo3_orange.svg'),
222 ),
223 'copyright' => BackendUtility::TYPO3_copyRightNotice(),
224 'loginNewsItems' => $this->getSystemNews(),
225 'loginProviderIdentifier' => $this->loginProviderIdentifier,
226 'loginProviders' => $this->loginProviders
227 ));
228
229 // Initialize interface selectors:
230 $this->makeInterfaceSelectorBox();
231
232 /** @var LoginProviderInterface $loginProvider */
233 $loginProvider = GeneralUtility::makeInstance($this->loginProviders[$this->loginProviderIdentifier]['provider']);
234 $loginProvider->render($this->view, $pageRenderer, $this);
235
236 $content = $this->getDocumentTemplate()->startPage('TYPO3 CMS Login: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], FALSE);
237 $content .= $this->view->render();
238 $content .= $this->getDocumentTemplate()->endPage();
239
240 return $content;
241 }
242
243 /**
244 * Checking, if we should perform some sort of redirection OR closing of windows.
245 *
246 * Do redirect:
247 *
248 * If a user is logged in AND
249 * a) if either the login is just done (isLoginInProgress) or
250 * b) a loginRefresh is done
251 *
252 * @throws \RuntimeException
253 * @throws \UnexpectedValueException
254 */
255 protected function checkRedirect() {
256 if (
257 empty($this->getBackendUserAuthentication()->user['uid'])
258 && ($this->isLoginInProgress() || !$this->loginRefresh)
259 ) {
260 return;
261 }
262
263 /*
264 * If no cookie has been set previously, we tell people that this is a problem.
265 * This assumes that a cookie-setting script (like this one) has been hit at
266 * least once prior to this instance.
267 */
268 if (!$_COOKIE[BackendUserAuthentication::getCookieName()]) {
269 if ($this->submitValue === 'setCookie') {
270 /*
271 * we tried it a second time but still no cookie
272 * 26/4 2005: This does not work anymore, because the saving of challenge values
273 * in $_SESSION means the system will act as if the password was wrong.
274 */
275 throw new \RuntimeException('Login-error: Yeah, that\'s a classic. No cookies, no TYPO3. ' .
276 'Please accept cookies from TYPO3 - otherwise you\'ll not be able to use the system.', 1294586846);
277 } else {
278 // try it once again - that might be needed for auto login
279 $this->redirectToURL = 'index.php?commandLI=setCookie';
280 }
281 }
282 $redirectToUrl = (string)$this->getBackendUserAuthentication()->getTSConfigVal('auth.BE.redirectToURL');
283 if (empty($redirectToUrl)) {
284 // Based on the interface we set the redirect script
285 switch (GeneralUtility::_GP('interface')) {
286 case 'frontend':
287 $interface = 'frontend';
288 $this->redirectToURL = '../';
289 break;
290 case 'backend':
291 $interface = 'backend';
292 $this->redirectToURL = 'backend.php';
293 break;
294 default:
295 $interface = '';
296 }
297 } else {
298 $this->redirectToURL = $redirectToUrl;
299 $interface = '';
300 }
301 // store interface
302 $this->getBackendUserAuthentication()->uc['interfaceSetup'] = $interface;
303 $this->getBackendUserAuthentication()->writeUC();
304
305 $formProtection = FormProtectionFactory::get();
306 if (!$formProtection instanceof BackendFormProtection) {
307 throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411);
308 }
309 if ($this->loginRefresh) {
310 $formProtection->setSessionTokenFromRegistry();
311 $formProtection->persistSessionToken();
312 $this->getDocumentTemplate()->JScode .= $this->getDocumentTemplate()->wrapScriptTags('
313 if (parent.opener && parent.opener.TYPO3 && parent.opener.TYPO3.LoginRefresh) {
314 parent.opener.TYPO3.LoginRefresh.startTask();
315 parent.close();
316 }
317 ');
318 } else {
319 $formProtection->storeSessionTokenInRegistry();
320 HttpUtility::redirect($this->redirectToURL);
321 }
322 }
323
324 /**
325 * Making interface selector:
326 *
327 * @return void
328 */
329 public function makeInterfaceSelectorBox() {
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 = array(
336 'backend' => array(
337 'label' => $this->getLanguageService()->getLL('interface.backend'),
338 'jumpScript' => 'backend.php',
339 'interface' => 'backend'
340 ),
341 'frontend' => array(
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 $systemNewsTable = 'sys_news';
366 $systemNews = array();
367 $systemNewsRecords = $this->getDatabaseConnection()->exec_SELECTgetRows('title, content, crdate', $systemNewsTable, '1=1' . BackendUtility::BEenableFields($systemNewsTable) . BackendUtility::deleteClause($systemNewsTable), '', 'crdate DESC');
368 foreach ($systemNewsRecords as $systemNewsRecord) {
369 $systemNews[] = array(
370 'date' => date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $systemNewsRecord['crdate']),
371 'header' => $systemNewsRecord['title'],
372 'content' => $systemNewsRecord['content']
373 );
374 }
375 return $systemNews;
376 }
377
378 /**
379 * Returns the uri of a relative reference, resolves the "EXT:" prefix
380 * (way of referring to files inside extensions) and checks that the file is inside
381 * the PATH_site of the TYPO3 installation
382 *
383 * @param string $filename The input filename/filepath to evaluate
384 * @return string Returns the filename of $filename if valid, otherwise blank string.
385 * @internal
386 */
387 private function getUriForFileName($filename) {
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 $username = GeneralUtility::_GP('username');
411 return !empty($username) || !empty($this->submitValue);
412 }
413
414 /**
415 * returns a new standalone view, shorthand function
416 *
417 * @return StandaloneView
418 */
419 protected function getFluidTemplateObject() {
420 /** @var StandaloneView $view */
421 $view = GeneralUtility::makeInstance(StandaloneView::class);
422 $view->setLayoutRootPaths(array(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')));
423 $view->setPartialRootPaths(array(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')));
424 $view->setTemplateRootPaths(array(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')));
425
426 $view->getRequest()->setControllerExtensionName('Backend');
427 return $view;
428 }
429
430 /**
431 * Validates the registered login providers
432 *
433 * @throws \RuntimeException
434 */
435 protected function validateAndSortLoginProviders() {
436 if (
437 !isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'])
438 || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'])
439 || empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'])
440 ) {
441 throw new \RuntimeException('No login providers are registered in $GLOBALS[\'TYPO3_CONF_VARS\'][\'EXTCONF\'][\'backend\'][\'loginProviders\'].', 1433417281);
442 }
443 $providers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'];
444 foreach ($providers as $identifier => $configuration) {
445 if (empty($configuration) || !is_array($configuration)) {
446 throw new \RuntimeException('Missing configuration for login provider "' . $identifier . '".', 1433416043);
447 }
448 if (!is_string($configuration['provider']) || empty($configuration['provider']) || !class_exists($configuration['provider']) || !is_subclass_of($configuration['provider'], LoginProviderInterface::class)) {
449 throw new \RuntimeException('The login provider "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . LoginProviderInterface::class . '".', 1433416043);
450 }
451 if (empty($configuration['label'])) {
452 throw new \RuntimeException('Missing label definition for login provider "' . $identifier . '".', 1433416044);
453 }
454 if (empty($configuration['icon-class'])) {
455 throw new \RuntimeException('Missing icon definition for login provider "' . $identifier . '".', 1433416045);
456 }
457 if (!isset($configuration['sorting'])) {
458 throw new \RuntimeException('Missing sorting definition for login provider "' . $identifier . '".', 1433416046);
459 }
460 }
461 // sort providers
462 uasort($providers, function($a, $b) {
463 return $b['sorting'] - $a['sorting'];
464 });
465 $this->loginProviders = $providers;
466 }
467
468 /**
469 * Detect the login provider, get from request or choose the
470 * first one as default
471 *
472 * @return string
473 */
474 protected function detectLoginProvider() {
475 $loginProvider = GeneralUtility::_GP('loginProvider');
476 if ((empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) && !empty($_COOKIE['be_lastLoginProvider'])) {
477 $loginProvider = $_COOKIE['be_lastLoginProvider'];
478 }
479 if (empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) {
480 reset($this->loginProviders);
481 $loginProvider = key($this->loginProviders);
482 }
483 setcookie('be_lastLoginProvider', $loginProvider, $GLOBALS['EXEC_TIME'] + 7776000); // 90 days
484 return $loginProvider;
485 }
486
487 /**
488 * @return string
489 */
490 public function getLoginProviderIdentifier() {
491 return $this->loginProviderIdentifier;
492 }
493
494 /**
495 * Returns LanguageService
496 *
497 * @return \TYPO3\CMS\Lang\LanguageService
498 */
499 protected function getLanguageService() {
500 return $GLOBALS['LANG'];
501 }
502
503 /**
504 * @return BackendUserAuthentication
505 */
506 protected function getBackendUserAuthentication() {
507 return $GLOBALS['BE_USER'];
508 }
509
510 /**
511 * Returns the database connection
512 *
513 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
514 */
515 protected function getDatabaseConnection() {
516 return $GLOBALS['TYPO3_DB'];
517 }
518
519 /**
520 * Returns an instance of DocumentTemplate
521 *
522 * @return \TYPO3\CMS\Backend\Template\DocumentTemplate
523 */
524 protected function getDocumentTemplate() {
525 return $GLOBALS['TBE_TEMPLATE'];
526 }
527
528 }