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