[TASK] Convert BackendAuthentication to Doctrine API
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Authentication / AbstractUserAuthentication.php
1 <?php
2 namespace TYPO3\CMS\Core\Authentication;
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\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Crypto\Random;
19 use TYPO3\CMS\Core\Database\Connection;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\DatabaseConnection;
22 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
23 use TYPO3\CMS\Core\Database\Query\QueryHelper;
24 use TYPO3\CMS\Core\Exception;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Core\Utility\MathUtility;
27
28 /**
29 * Authentication of users in TYPO3
30 *
31 * This class is used to authenticate a login user.
32 * The class is used by both the frontend and backend.
33 * In both cases this class is a parent class to BackendUserAuthentication and FrontenUserAuthentication
34 *
35 * See Inside TYPO3 for more information about the API of the class and internal variables.
36 */
37 abstract class AbstractUserAuthentication
38 {
39 /**
40 * Table to use for session data
41 * @var string
42 */
43 public $session_table = '';
44
45 /**
46 * Session/Cookie name
47 * @var string
48 */
49 public $name = '';
50
51 /**
52 * Session/GET-var name
53 * @var string
54 */
55 public $get_name = '';
56
57 /**
58 * Table in database with user data
59 * @var string
60 */
61 public $user_table = '';
62
63 /**
64 * Table in database with user groups
65 * @var string
66 */
67 public $usergroup_table = '';
68
69 /**
70 * Column for login-name
71 * @var string
72 */
73 public $username_column = '';
74
75 /**
76 * Column for password
77 * @var string
78 */
79 public $userident_column = '';
80
81 /**
82 * Column for user-id
83 * @var string
84 */
85 public $userid_column = '';
86
87 /**
88 * Column for user group information
89 * @var string
90 */
91 public $usergroup_column = '';
92
93 /**
94 * Column name for last login timestamp
95 * @var string
96 */
97 public $lastLogin_column = '';
98
99 /**
100 * Enable field columns of user table
101 * @var array
102 */
103 public $enablecolumns = array(
104 'rootLevel' => '',
105 // Boolean: If TRUE, 'AND pid=0' will be a part of the query...
106 'disabled' => '',
107 'starttime' => '',
108 'endtime' => '',
109 'deleted' => ''
110 );
111
112 /**
113 * @var bool
114 */
115 public $showHiddenRecords = false;
116
117 /**
118 * Form field with login-name
119 * @var string
120 */
121 public $formfield_uname = '';
122
123 /**
124 * Form field with password
125 * @var string
126 */
127 public $formfield_uident = '';
128
129 /**
130 * Form field with status: *'login', 'logout'. If empty login is not verified.
131 * @var string
132 */
133 public $formfield_status = '';
134
135 /**
136 * Session timeout (on the server)
137 *
138 * If >0: session-timeout in seconds.
139 * If 0: no timeout.
140 *
141 * @var int
142 */
143 public $sessionTimeout = 0;
144
145 /**
146 * Name for a field to fetch the server session timeout from.
147 * If not empty this is a field name from the user table where the timeout can be found.
148 * @var string
149 */
150 public $auth_timeout_field = '';
151
152 /**
153 * Lifetime for the session-cookie (on the client)
154 *
155 * If >0: permanent cookie with given lifetime
156 * If 0: session-cookie
157 * Session-cookie means the browser will remove it when the browser is closed.
158 *
159 * @var int
160 */
161 public $lifetime = 0;
162
163 /**
164 * GarbageCollection
165 * Purge all server session data older than $gc_time seconds.
166 * 0 = default to $this->sessionTimeout or use 86400 seconds (1 day) if $this->sessionTimeout == 0
167 * @var int
168 */
169 public $gc_time = 0;
170
171 /**
172 * Probability for g arbage collection to be run (in percent)
173 * @var int
174 */
175 public $gc_probability = 1;
176
177 /**
178 * Decides if the writelog() function is called at login and logout
179 * @var bool
180 */
181 public $writeStdLog = false;
182
183 /**
184 * Log failed login attempts
185 * @var bool
186 */
187 public $writeAttemptLog = false;
188
189 /**
190 * Send no-cache headers
191 * @var bool
192 */
193 public $sendNoCacheHeaders = true;
194
195 /**
196 * If this is set, authentication is also accepted by $_GET.
197 * Notice that the identification is NOT 128bit MD5 hash but reduced.
198 * This is done in order to minimize the size for mobile-devices, such as WAP-phones
199 * @var bool
200 */
201 public $getFallBack = false;
202
203 /**
204 * The ident-hash is normally 32 characters and should be!
205 * But if you are making sites for WAP-devices or other low-bandwidth stuff,
206 * you may shorten the length.
207 * Never let this value drop below 6!
208 * A length of 6 would give you more than 16 mio possibilities.
209 * @var int
210 */
211 public $hash_length = 32;
212
213 /**
214 * Setting this flag TRUE lets user-authentication happen from GET_VARS if
215 * POST_VARS are not set. Thus you may supply username/password with the URL.
216 * @var bool
217 */
218 public $getMethodEnabled = false;
219
220 /**
221 * If set to 4, the session will be locked to the user's IP address (all four numbers).
222 * Reducing this to 1-3 means that only the given number of parts of the IP address is used.
223 * @var int
224 */
225 public $lockIP = 4;
226
227 /**
228 * Keyword list (comma separated list with no spaces!)
229 * Each keyword indicates some information that can be included in a hash made to lock down user sessions.
230 * Configurable by $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['lockHashKeyWords']
231 * @var string
232 */
233 public $lockHashKeyWords = 'useragent';
234
235 /**
236 * @var string
237 */
238 public $warningEmail = '';
239
240 /**
241 * Time span (in seconds) within the number of failed logins are collected
242 * @var int
243 */
244 public $warningPeriod = 3600;
245
246 /**
247 * The maximum accepted number of warnings before an email to $warningEmail is sent
248 * @var int
249 */
250 public $warningMax = 3;
251
252 /**
253 * If set, the user-record must be stored at the page defined by $checkPid_value
254 * @var bool
255 */
256 public $checkPid = true;
257
258 /**
259 * The page id the user record must be stored at
260 * @var int
261 */
262 public $checkPid_value = 0;
263
264 /**
265 * session_id (MD5-hash)
266 * @var string
267 * @internal
268 */
269 public $id;
270
271 /**
272 * Indicates if an authentication was started but failed
273 * @var bool
274 */
275 public $loginFailure = false;
276
277 /**
278 * Will be set to TRUE if the login session is actually written during auth-check.
279 * @var bool
280 */
281 public $loginSessionStarted = false;
282
283 /**
284 * @var array|NULL contains user- AND session-data from database (joined tables)
285 * @internal
286 */
287 public $user = null;
288
289 /**
290 * Will be added to the url (eg. '&login=ab7ef8d...')
291 * GET-auth-var if getFallBack is TRUE. Should be inserted in links!
292 * @var string
293 * @internal
294 */
295 public $get_URL_ID = '';
296
297 /**
298 * Will be set to TRUE if a new session ID was created
299 * @var bool
300 */
301 public $newSessionID = false;
302
303 /**
304 * Will force the session cookie to be set every time (lifetime must be 0)
305 * @var bool
306 */
307 public $forceSetCookie = false;
308
309 /**
310 * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
311 * @var bool
312 */
313 public $dontSetCookie = false;
314
315 /**
316 * @var bool
317 */
318 protected $cookieWasSetOnCurrentRequest = false;
319
320 /**
321 * Login type, used for services.
322 * @var string
323 */
324 public $loginType = '';
325
326 /**
327 * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']
328 * @var array
329 */
330 public $svConfig = array();
331
332 /**
333 * Write messages to the devlog
334 * @var bool
335 */
336 public $writeDevLog = false;
337
338 /**
339 * @var array
340 */
341 public $uc;
342
343 /**
344 * @var DatabaseConnection
345 */
346 protected $db = null;
347
348 /**
349 * Initialize some important variables
350 */
351 public function __construct()
352 {
353 $this->db = $this->getDatabaseConnection();
354 }
355
356 /**
357 * Starts a user session
358 * Typical configurations will:
359 * a) check if session cookie was set and if not, set one,
360 * b) check if a password/username was sent and if so, try to authenticate the user
361 * c) Lookup a session attached to a user and check timeout etc.
362 * d) Garbage collection, setting of no-cache headers.
363 * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable.
364 *
365 * @throws Exception
366 * @return void
367 */
368 public function start()
369 {
370 // Backend or frontend login - used for auth services
371 if (empty($this->loginType)) {
372 throw new Exception('No loginType defined, should be set explicitly by subclass');
373 }
374 // Enable dev logging if set
375 if ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['writeDevLog']) {
376 $this->writeDevLog = true;
377 }
378 if ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['writeDevLog' . $this->loginType]) {
379 $this->writeDevLog = true;
380 }
381 if (TYPO3_DLOG) {
382 $this->writeDevLog = true;
383 }
384 if ($this->writeDevLog) {
385 GeneralUtility::devLog('## Beginning of auth logging.', AbstractUserAuthentication::class);
386 }
387 // Init vars.
388 $mode = '';
389 $this->newSessionID = false;
390 // $id is set to ses_id if cookie is present. Else set to FALSE, which will start a new session
391 $id = $this->getCookie($this->name);
392 $this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'];
393
394 // If fallback to get mode....
395 if (!$id && $this->getFallBack && $this->get_name) {
396 $id = isset($_GET[$this->get_name]) ? GeneralUtility::_GET($this->get_name) : '';
397 if (strlen($id) != $this->hash_length) {
398 $id = '';
399 }
400 $mode = 'get';
401 }
402
403 // If new session or client tries to fix session...
404 if (!$id || !$this->isExistingSessionRecord($id)) {
405 // New random session-$id is made
406 $id = $this->createSessionId();
407 // New session
408 $this->newSessionID = true;
409 }
410 // Internal var 'id' is set
411 $this->id = $id;
412 // If fallback to get mode....
413 if ($mode == 'get' && $this->getFallBack && $this->get_name) {
414 $this->get_URL_ID = '&' . $this->get_name . '=' . $id;
415 }
416 // Set session hashKey lock keywords from configuration; currently only 'useragent' can be used.
417 $this->lockHashKeyWords = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['lockHashKeyWords'];
418 // Make certain that NO user is set initially
419 $this->user = null;
420 // Set all possible headers that could ensure that the script is not cached on the client-side
421 if ($this->sendNoCacheHeaders && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI)) {
422 header('Expires: 0');
423 header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
424 $cacheControlHeader = 'no-cache, must-revalidate';
425 $pragmaHeader = 'no-cache';
426 // Prevent error message in IE when using a https connection
427 // see http://forge.typo3.org/issues/24125
428 $clientInfo = GeneralUtility::clientInfo();
429 if ($clientInfo['BROWSER'] === 'msie' && GeneralUtility::getIndpEnv('TYPO3_SSL')) {
430 // Some IEs can not handle no-cache
431 // see http://support.microsoft.com/kb/323308/en-us
432 $cacheControlHeader = 'must-revalidate';
433 // IE needs "Pragma: private" if SSL connection
434 $pragmaHeader = 'private';
435 }
436 header('Cache-Control: ' . $cacheControlHeader);
437 header('Pragma: ' . $pragmaHeader);
438 }
439 // Load user session, check to see if anyone has submitted login-information and if so authenticate
440 // the user with the session. $this->user[uid] may be used to write log...
441 $this->checkAuthentication();
442 // Setting cookies
443 if (!$this->dontSetCookie) {
444 $this->setSessionCookie();
445 }
446 // Hook for alternative ways of filling the $this->user array (is used by the "timtaw" extension)
447 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'])) {
448 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] as $funcName) {
449 $_params = array(
450 'pObj' => &$this
451 );
452 GeneralUtility::callUserFunction($funcName, $_params, $this);
453 }
454 }
455 // Set $this->gc_time if not explicitly specified
456 if ($this->gc_time === 0) {
457 // Default to 86400 seconds (1 day) if $this->sessionTimeout is 0
458 $this->gc_time = $this->sessionTimeout === 0 ? 86400 : $this->sessionTimeout;
459 }
460 // If we're lucky we'll get to clean up old sessions
461 if (rand() % 100 <= $this->gc_probability) {
462 $this->gc();
463 }
464 }
465
466 /**
467 * Sets the session cookie for the current disposal.
468 *
469 * @return void
470 * @throws Exception
471 */
472 protected function setSessionCookie()
473 {
474 $isSetSessionCookie = $this->isSetSessionCookie();
475 $isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie();
476 if ($isSetSessionCookie || $isRefreshTimeBasedCookie) {
477 $settings = $GLOBALS['TYPO3_CONF_VARS']['SYS'];
478 // Get the domain to be used for the cookie (if any):
479 $cookieDomain = $this->getCookieDomain();
480 // If no cookie domain is set, use the base path:
481 $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
482 // If the cookie lifetime is set, use it:
483 $cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0;
484 // Use the secure option when the current request is served by a secure connection:
485 $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL');
486 // Deliver cookies only via HTTP and prevent possible XSS by JavaScript:
487 $cookieHttpOnly = (bool)$settings['cookieHttpOnly'];
488 // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used:
489 if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility::getIndpEnv('TYPO3_SSL')) {
490 setcookie($this->name, $this->id, $cookieExpire, $cookiePath, $cookieDomain, $cookieSecure, $cookieHttpOnly);
491 $this->cookieWasSetOnCurrentRequest = true;
492 } else {
493 throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546);
494 }
495 if ($this->writeDevLog) {
496 $devLogMessage = ($isRefreshTimeBasedCookie ? 'Updated Cookie: ' : 'Set Cookie: ') . $this->id;
497 GeneralUtility::devLog($devLogMessage . ($cookieDomain ? ', ' . $cookieDomain : ''), AbstractUserAuthentication::class);
498 }
499 }
500 }
501
502 /**
503 * Gets the domain to be used on setting cookies.
504 * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'].
505 *
506 * @return string The domain to be used on setting cookies
507 */
508 protected function getCookieDomain()
509 {
510 $result = '';
511 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'];
512 // If a specific cookie domain is defined for a given TYPO3_MODE,
513 // use that domain
514 if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'])) {
515 $cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'];
516 }
517 if ($cookieDomain) {
518 if ($cookieDomain[0] == '/') {
519 $match = array();
520 $matchCnt = @preg_match($cookieDomain, GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), $match);
521 if ($matchCnt === false) {
522 GeneralUtility::sysLog('The regular expression for the cookie domain (' . $cookieDomain . ') contains errors. The session is not shared across sub-domains.', 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
523 } elseif ($matchCnt) {
524 $result = $match[0];
525 }
526 } else {
527 $result = $cookieDomain;
528 }
529 }
530 return $result;
531 }
532
533 /**
534 * Get the value of a specified cookie.
535 *
536 * @param string $cookieName The cookie ID
537 * @return string The value stored in the cookie
538 */
539 protected function getCookie($cookieName)
540 {
541 return isset($_COOKIE[$cookieName]) ? stripslashes($_COOKIE[$cookieName]) : '';
542 }
543
544 /**
545 * Determine whether a session cookie needs to be set (lifetime=0)
546 *
547 * @return bool
548 * @internal
549 */
550 public function isSetSessionCookie()
551 {
552 return ($this->newSessionID || $this->forceSetCookie) && $this->lifetime == 0;
553 }
554
555 /**
556 * Determine whether a non-session cookie needs to be set (lifetime>0)
557 *
558 * @return bool
559 * @internal
560 */
561 public function isRefreshTimeBasedCookie()
562 {
563 return $this->lifetime > 0;
564 }
565
566 /**
567 * Checks if a submission of username and password is present or use other authentication by auth services
568 *
569 * @throws \RuntimeException
570 * @return void
571 * @internal
572 */
573 public function checkAuthentication()
574 {
575 // No user for now - will be searched by service below
576 $tempuserArr = array();
577 $tempuser = false;
578 // User is not authenticated by default
579 $authenticated = false;
580 // User want to login with passed login data (name/password)
581 $activeLogin = false;
582 // Indicates if an active authentication failed (not auto login)
583 $this->loginFailure = false;
584 if ($this->writeDevLog) {
585 GeneralUtility::devLog('Login type: ' . $this->loginType, AbstractUserAuthentication::class);
586 }
587 // The info array provide additional information for auth services
588 $authInfo = $this->getAuthInfoArray();
589 // Get Login/Logout data submitted by a form or params
590 $loginData = $this->getLoginFormData();
591 if ($this->writeDevLog) {
592 GeneralUtility::devLog('Login data: ' . GeneralUtility::arrayToLogString($loginData), AbstractUserAuthentication::class);
593 }
594 // Active logout (eg. with "logout" button)
595 if ($loginData['status'] === 'logout') {
596 if ($this->writeStdLog) {
597 // $type,$action,$error,$details_nr,$details,$data,$tablename,$recuid,$recpid
598 $this->writelog(255, 2, 0, 2, 'User %s logged out', array($this->user['username']), '', 0, 0);
599 }
600 // Logout written to log
601 if ($this->writeDevLog) {
602 GeneralUtility::devLog('User logged out. Id: ' . $this->id, AbstractUserAuthentication::class, -1);
603 }
604 $this->logoff();
605 }
606 // Active login (eg. with login form)
607 if ($loginData['status'] === 'login') {
608 $activeLogin = true;
609 if ($this->writeDevLog) {
610 GeneralUtility::devLog('Active login (eg. with login form)', AbstractUserAuthentication::class);
611 }
612 // check referer for submitted login values
613 if ($this->formfield_status && $loginData['uident'] && $loginData['uname']) {
614 $httpHost = GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY');
615 if (!$this->getMethodEnabled && ($httpHost != $authInfo['refInfo']['host'] && !$GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer'])) {
616 throw new \RuntimeException('TYPO3 Fatal Error: Error: This host address ("' . $httpHost . '") and the referer host ("' . $authInfo['refInfo']['host'] . '") mismatches! ' .
617 'It is possible that the environment variable HTTP_REFERER is not passed to the script because of a proxy. ' .
618 'The site administrator can disable this check in the "All Configuration" section of the Install Tool (flag: TYPO3_CONF_VARS[SYS][doNotCheckReferer]).', 1270853930);
619 }
620 // Delete old user session if any
621 $this->logoff();
622 }
623 // Refuse login for _CLI users, if not processing a CLI request type
624 // (although we shouldn't be here in case of a CLI request type)
625 if (strtoupper(substr($loginData['uname'], 0, 5)) == '_CLI_' && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI)) {
626 throw new \RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931);
627 }
628 }
629 // The following code makes auto-login possible (if configured). No submitted data needed
630 // Determine whether we need to skip session update.
631 // This is used mainly for checking session timeout without
632 // refreshing the session itself while checking.
633 $skipSessionUpdate = (bool)GeneralUtility::_GP('skipSessionUpdate');
634 $haveSession = false;
635 if (!$this->newSessionID) {
636 // Read user session
637 $authInfo['userSession'] = $this->fetchUserSession($skipSessionUpdate);
638 $haveSession = is_array($authInfo['userSession']);
639 }
640 if ($this->writeDevLog) {
641 if ($haveSession) {
642 GeneralUtility::devLog('User session found: ' . GeneralUtility::arrayToLogString($authInfo['userSession'], array($this->userid_column, $this->username_column)), AbstractUserAuthentication::class, 0);
643 } else {
644 GeneralUtility::devLog('No user session found.', AbstractUserAuthentication::class, 2);
645 }
646 if (is_array($this->svConfig['setup'])) {
647 GeneralUtility::devLog('SV setup: ' . GeneralUtility::arrayToLogString($this->svConfig['setup']), AbstractUserAuthentication::class, 0);
648 }
649 }
650 // Fetch user if ...
651 if (
652 $activeLogin || $this->svConfig['setup'][$this->loginType . '_alwaysFetchUser']
653 || !$haveSession && $this->svConfig['setup'][$this->loginType . '_fetchUserIfNoSession']
654 ) {
655 // Use 'auth' service to find the user
656 // First found user will be used
657 $serviceChain = '';
658 $subType = 'getUser' . $this->loginType;
659 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
660 $serviceChain .= ',' . $serviceObj->getServiceKey();
661 $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
662 if ($row = $serviceObj->getUser()) {
663 $tempuserArr[] = $row;
664 if ($this->writeDevLog) {
665 GeneralUtility::devLog('User found: ' . GeneralUtility::arrayToLogString($row, array($this->userid_column, $this->username_column)), AbstractUserAuthentication::class, 0);
666 }
667 // User found, just stop to search for more if not configured to go on
668 if (!$this->svConfig['setup'][$this->loginType . '_fetchAllUsers']) {
669 break;
670 }
671 }
672 unset($serviceObj);
673 }
674 unset($serviceObj);
675 if ($this->writeDevLog && $this->svConfig['setup'][$this->loginType . '_alwaysFetchUser']) {
676 GeneralUtility::devLog($this->loginType . '_alwaysFetchUser option is enabled', AbstractUserAuthentication::class);
677 }
678 if ($this->writeDevLog && $serviceChain) {
679 GeneralUtility::devLog($subType . ' auth services called: ' . $serviceChain, AbstractUserAuthentication::class);
680 }
681 if ($this->writeDevLog && empty($tempuserArr)) {
682 GeneralUtility::devLog('No user found by services', AbstractUserAuthentication::class);
683 }
684 if ($this->writeDevLog && !empty($tempuserArr)) {
685 GeneralUtility::devLog(count($tempuserArr) . ' user records found by services', AbstractUserAuthentication::class);
686 }
687 }
688 // If no new user was set we use the already found user session
689 if (empty($tempuserArr) && $haveSession) {
690 $tempuserArr[] = $authInfo['userSession'];
691 $tempuser = $authInfo['userSession'];
692 // User is authenticated because we found a user session
693 $authenticated = true;
694 if ($this->writeDevLog) {
695 GeneralUtility::devLog('User session used: ' . GeneralUtility::arrayToLogString($authInfo['userSession'], array($this->userid_column, $this->username_column)), AbstractUserAuthentication::class);
696 }
697 }
698 // Re-auth user when 'auth'-service option is set
699 if ($this->svConfig['setup'][$this->loginType . '_alwaysAuthUser']) {
700 $authenticated = false;
701 if ($this->writeDevLog) {
702 GeneralUtility::devLog('alwaysAuthUser option is enabled', AbstractUserAuthentication::class);
703 }
704 }
705 // Authenticate the user if needed
706 if (!empty($tempuserArr) && !$authenticated) {
707 foreach ($tempuserArr as $tempuser) {
708 // Use 'auth' service to authenticate the user
709 // If one service returns FALSE then authentication failed
710 // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service
711 if ($this->writeDevLog) {
712 GeneralUtility::devLog('Auth user: ' . GeneralUtility::arrayToLogString($tempuser), AbstractUserAuthentication::class);
713 }
714 $serviceChain = '';
715 $subType = 'authUser' . $this->loginType;
716 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
717 $serviceChain .= ',' . $serviceObj->getServiceKey();
718 $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
719 if (($ret = $serviceObj->authUser($tempuser)) > 0) {
720 // If the service returns >=200 then no more checking is needed - useful for IP checking without password
721 if ((int)$ret >= 200) {
722 $authenticated = true;
723 break;
724 } elseif ((int)$ret >= 100) {
725 } else {
726 $authenticated = true;
727 }
728 } else {
729 $authenticated = false;
730 break;
731 }
732 unset($serviceObj);
733 }
734 unset($serviceObj);
735 if ($this->writeDevLog && $serviceChain) {
736 GeneralUtility::devLog($subType . ' auth services called: ' . $serviceChain, AbstractUserAuthentication::class);
737 }
738 if ($authenticated) {
739 // Leave foreach() because a user is authenticated
740 break;
741 }
742 }
743 }
744 // If user is authenticated a valid user is in $tempuser
745 if ($authenticated) {
746 // Reset failure flag
747 $this->loginFailure = false;
748 // Insert session record if needed:
749 if (!($haveSession && ($tempuser['ses_id'] == $this->id || $tempuser['uid'] == $authInfo['userSession']['ses_userid']))) {
750 $sessionData = $this->createUserSession($tempuser);
751 if ($sessionData) {
752 $this->user = array_merge(
753 $tempuser,
754 $sessionData
755 );
756 }
757 // The login session is started.
758 $this->loginSessionStarted = true;
759 if ($this->writeDevLog && is_array($this->user)) {
760 GeneralUtility::devLog('User session finally read: ' . GeneralUtility::arrayToLogString($this->user, array($this->userid_column, $this->username_column)), AbstractUserAuthentication::class, -1);
761 }
762 } elseif ($haveSession) {
763 $this->user = $authInfo['userSession'];
764 }
765 if ($activeLogin && !$this->newSessionID) {
766 $this->regenerateSessionId();
767 }
768 // User logged in - write that to the log!
769 if ($this->writeStdLog && $activeLogin) {
770 $this->writelog(255, 1, 0, 1, 'User %s logged in from %s (%s)', array($tempuser[$this->username_column], GeneralUtility::getIndpEnv('REMOTE_ADDR'), GeneralUtility::getIndpEnv('REMOTE_HOST')), '', '', '', -1, '', $tempuser['uid']);
771 }
772 if ($this->writeDevLog && $activeLogin) {
773 GeneralUtility::devLog('User ' . $tempuser[$this->username_column] . ' logged in from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR') . ' (' . GeneralUtility::getIndpEnv('REMOTE_HOST') . ')', AbstractUserAuthentication::class, -1);
774 }
775 if ($this->writeDevLog && !$activeLogin) {
776 GeneralUtility::devLog('User ' . $tempuser[$this->username_column] . ' authenticated from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR') . ' (' . GeneralUtility::getIndpEnv('REMOTE_HOST') . ')', AbstractUserAuthentication::class, -1);
777 }
778 } elseif ($activeLogin || !empty($tempuserArr)) {
779 $this->loginFailure = true;
780 if ($this->writeDevLog && empty($tempuserArr) && $activeLogin) {
781 GeneralUtility::devLog('Login failed: ' . GeneralUtility::arrayToLogString($loginData), AbstractUserAuthentication::class, 2);
782 }
783 if ($this->writeDevLog && !empty($tempuserArr)) {
784 GeneralUtility::devLog('Login failed: ' . GeneralUtility::arrayToLogString($tempuser, array($this->userid_column, $this->username_column)), AbstractUserAuthentication::class, 2);
785 }
786 }
787 // If there were a login failure, check to see if a warning email should be sent:
788 if ($this->loginFailure && $activeLogin) {
789 if ($this->writeDevLog) {
790 GeneralUtility::devLog('Call checkLogFailures: ' . GeneralUtility::arrayToLogString(array('warningEmail' => $this->warningEmail, 'warningPeriod' => $this->warningPeriod, 'warningMax' => $this->warningMax)), AbstractUserAuthentication::class, -1);
791 }
792
793 // Hook to implement login failure tracking methods
794 if (
795 !empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'])
796 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'])
797 ) {
798 $_params = array();
799 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] as $_funcRef) {
800 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
801 }
802 } else {
803 // If no hook is implemented, wait for 5 seconds
804 sleep(5);
805 }
806
807 $this->checkLogFailures($this->warningEmail, $this->warningPeriod, $this->warningMax);
808 }
809 }
810
811 /**
812 * Creates a new session ID.
813 *
814 * @return string The new session ID
815 */
816 public function createSessionId()
817 {
818 return GeneralUtility::makeInstance(Random::class)->generateRandomHexString($this->hash_length);
819 }
820
821 /**
822 * Regenerate the session ID and transfer the session to new ID
823 * Call this method whenever a user proceeds to a higher authorization level
824 * e.g. when an anonymous session is now authenticated.
825 */
826 protected function regenerateSessionId()
827 {
828 $oldSessionId = $this->id;
829 $this->id = $this->createSessionId();
830 // Update session record with new ID
831 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table)->update(
832 $this->session_table,
833 ['ses_id' => $this->id],
834 ['ses_id' => $oldSessionId, 'ses_name' => $this->name]
835 );
836 $this->newSessionID = true;
837 }
838
839 /*************************
840 *
841 * User Sessions
842 *
843 *************************/
844 /**
845 * Creates a user session record and returns its values.
846 *
847 * @param array $tempuser User data array
848 *
849 * @return array The session data for the newly created session.
850 */
851 public function createUserSession($tempuser)
852 {
853 if ($this->writeDevLog) {
854 GeneralUtility::devLog('Create session ses_id = ' . $this->id, AbstractUserAuthentication::class);
855 }
856 // Delete session entry first
857 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table);
858 $connection->delete(
859 $this->session_table,
860 ['ses_id' => $this->id, 'ses_name' => $this->name]
861 );
862
863 // Re-create session entry
864 $insertFields = $this->getNewSessionRecord($tempuser);
865 $inserted = (bool)$connection->insert($this->session_table, $insertFields);
866 if (!$inserted) {
867 $message = 'Session data could not be written to DB. Error: ' . $connection->errorInfo();
868 GeneralUtility::sysLog($message, 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
869 if ($this->writeDevLog) {
870 GeneralUtility::devLog($message, AbstractUserAuthentication::class, 2);
871 }
872 }
873 // Updating lastLogin_column carrying information about last login.
874 if ($this->lastLogin_column && $inserted) {
875 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
876 $connection->update(
877 $this->user_table,
878 [$this->lastLogin_column => $GLOBALS['EXEC_TIME']],
879 [$this->userid_column => $tempuser[$this->userid_column]]
880 );
881 }
882
883 return $inserted ? $insertFields : [];
884 }
885
886 /**
887 * Returns a new session record for the current user for insertion into the DB.
888 * This function is mainly there as a wrapper for inheriting classes to override it.
889 *
890 * @param array $tempuser
891 * @return array User session record
892 */
893 public function getNewSessionRecord($tempuser)
894 {
895 return array(
896 'ses_id' => $this->id,
897 'ses_name' => $this->name,
898 'ses_iplock' => $tempuser['disableIPlock'] ? '[DISABLED]' : $this->ipLockClause_remoteIPNumber($this->lockIP),
899 'ses_hashlock' => $this->hashLockClause_getHashInt(),
900 'ses_userid' => $tempuser[$this->userid_column],
901 'ses_tstamp' => $GLOBALS['EXEC_TIME'],
902 'ses_data' => ''
903 );
904 }
905
906 /**
907 * Read the user session from db.
908 *
909 * @param bool $skipSessionUpdate
910 * @return array User session data
911 */
912 public function fetchUserSession($skipSessionUpdate = false)
913 {
914 $user = false;
915 if ($this->writeDevLog) {
916 GeneralUtility::devLog('Fetch session ses_id = ' . $this->id, AbstractUserAuthentication::class);
917 }
918
919 // Fetch the user session from the DB
920 $user = $this->fetchUserSessionFromDB();
921
922 if ($user) {
923 // A user was found
924 $user['ses_tstamp'] = (int)$user['ses_tstamp'];
925
926 if (!empty($this->auth_timeout_field)) {
927 // Get timeout-time from usertable
928 $timeout = (int)$user[$this->auth_timeout_field];
929 } else {
930 $timeout = $this->sessionTimeout;
931 }
932 // If timeout > 0 (TRUE) and current time has not exceeded the latest sessions-time plus the timeout in seconds then accept user
933 // Use a gracetime-value to avoid updating a session-record too often
934 if ($timeout > 0 && $GLOBALS['EXEC_TIME'] < $user['ses_tstamp'] + $timeout) {
935 $sessionUpdateGracePeriod = 61;
936 if (!$skipSessionUpdate && $GLOBALS['EXEC_TIME'] > ($user['ses_tstamp'] + $sessionUpdateGracePeriod)) {
937 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table)->update(
938 $this->session_table,
939 ['ses_tstamp' => $GLOBALS['EXEC_TIME']],
940 ['ses_id' => $this->id, 'ses_name' => $this->name]
941 );
942 // Make sure that the timestamp is also updated in the array
943 $user['ses_tstamp'] = $GLOBALS['EXEC_TIME'];
944 }
945 } else {
946 // Delete any user set...
947 $this->logoff();
948 $user = false;
949 }
950 }
951 return $user;
952 }
953
954 /**
955 * Log out current user!
956 * Removes the current session record, sets the internal ->user array to a blank string;
957 * Thereby the current user (if any) is effectively logged out!
958 *
959 * @return void
960 */
961 public function logoff()
962 {
963 if ($this->writeDevLog) {
964 GeneralUtility::devLog('logoff: ses_id = ' . $this->id, AbstractUserAuthentication::class);
965 }
966 // Release the locked records
967 BackendUtility::lockRecords();
968 // Hook for pre-processing the logoff() method, requested and implemented by andreas.otto@dkd.de:
969 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'])) {
970 $_params = array();
971 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] as $_funcRef) {
972 if ($_funcRef) {
973 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
974 }
975 }
976 }
977
978 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table)->delete(
979 $this->session_table,
980 ['ses_id' => $this->id, 'ses_name' => $this->name]
981 );
982
983 $this->user = null;
984 // Hook for post-processing the logoff() method, requested and implemented by andreas.otto@dkd.de:
985 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'])) {
986 $_params = array();
987 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] as $_funcRef) {
988 if ($_funcRef) {
989 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
990 }
991 }
992 }
993 }
994
995 /**
996 * Empty / unset the cookie
997 *
998 * @param string $cookieName usually, this is $this->name
999 * @return void
1000 */
1001 public function removeCookie($cookieName)
1002 {
1003 $cookieDomain = $this->getCookieDomain();
1004 // If no cookie domain is set, use the base path
1005 $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
1006 setcookie($cookieName, null, -1, $cookiePath, $cookieDomain);
1007 }
1008
1009 /**
1010 * Determine whether there's an according session record to a given session_id
1011 * in the database. Don't care if session record is still valid or not.
1012 *
1013 * @param int $id Claimed Session ID
1014 * @return bool Returns TRUE if a corresponding session was found in the database
1015 */
1016 public function isExistingSessionRecord($id)
1017 {
1018 $conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table);
1019 $count = $conn->count(
1020 '*',
1021 $this->session_table,
1022 ['ses_id' => $id]
1023 );
1024
1025 return (bool)$count;
1026 }
1027
1028 /**
1029 * Returns whether this request is going to set a cookie
1030 * or a cookie was already found in the system
1031 *
1032 * @return bool Returns TRUE if a cookie is set
1033 */
1034 public function isCookieSet()
1035 {
1036 return $this->cookieWasSetOnCurrentRequest || $this->getCookie($this->name);
1037 }
1038
1039 /*************************
1040 *
1041 * SQL Functions
1042 *
1043 *************************/
1044 /**
1045 * The session_id is used to find user in the database.
1046 * Two tables are joined: The session-table with user_id of the session and the usertable with its primary key
1047 * if the client is flash (e.g. from a flash application inside TYPO3 that does a server request)
1048 * then don't evaluate with the hashLockClause, as the client/browser is included in this hash
1049 * and thus, the flash request would be rejected
1050 *
1051 * @return array|false
1052 * @access private
1053 */
1054 protected function fetchUserSessionFromDB()
1055 {
1056 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1057 ->getQueryBuilderForTable($this->session_table);
1058 $queryBuilder->select('*')
1059 ->from($this->session_table)
1060 ->from($this->user_table)
1061 ->where(
1062 $queryBuilder->expr()->eq(
1063 $this->session_table . '.ses_id',
1064 $queryBuilder->createNamedParameter($this->id)
1065 )
1066 )
1067 ->andWhere(
1068 $queryBuilder->expr()->eq(
1069 $this->session_table . '.ses_name',
1070 $queryBuilder->createNamedParameter($this->name)
1071 )
1072 )
1073 // Condition on which to join the session and user table
1074 ->andWhere(
1075 $queryBuilder->expr()->eq(
1076 $this->session_table . '.ses_userid',
1077 $queryBuilder->quoteIdentifier($this->user_table . '.' . $this->userid_column)
1078 )
1079 )
1080 ->andWhere(
1081 $queryBuilder->expr()->eq(
1082 $this->session_table . '.ses_hashlock',
1083 $queryBuilder->createNamedParameter($this->hashLockClause_getHashInt())
1084 )
1085 )
1086 ->andWhere($this->userConstraints($queryBuilder->expr()));
1087
1088 if ($this->lockIP) {
1089 $queryBuilder->andWhere(
1090 $queryBuilder->expr()->in(
1091 $this->session_table . '.ses_iplock',
1092 $queryBuilder->createNamedParameter(
1093 [$this->ipLockClause_remoteIPNumber($this->lockIP), '[DISABLED]'],
1094 Connection::PARAM_STR_ARRAY // Automatically expand the array into multiple named parameters
1095 )
1096 )
1097 );
1098 }
1099
1100 // Force the fetch mode to ensure we get back an array independently of the default fetch mode.
1101 return $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC);
1102 }
1103
1104 /**
1105 * @param ExpressionBuilder $expressionBuilder
1106 * @param string $tableAlias
1107 * @return \Doctrine\DBAL\Query\Expression\CompositeExpression
1108 * @internal
1109 */
1110 protected function userConstraints(
1111 ExpressionBuilder $expressionBuilder,
1112 string $tableAlias = ''
1113 ): \Doctrine\DBAL\Query\Expression\CompositeExpression {
1114 if ($tableAlias === '') {
1115 $tableAlias = $this->user_table;
1116 }
1117
1118 $constraints = $expressionBuilder->andX();
1119 if ($this->enablecolumns['rootLevel']) {
1120 $constraints->add(
1121 $expressionBuilder->eq($tableAlias . '.pid', 0)
1122 );
1123 }
1124 if ($this->enablecolumns['disabled']) {
1125 $constraints->add(
1126 $expressionBuilder->eq($tableAlias . '.' . $this->enablecolumns['disabled'], 0)
1127 );
1128 }
1129 if ($this->enablecolumns['deleted']) {
1130 $constraints->add(
1131 $expressionBuilder->eq($tableAlias . '.' . $this->enablecolumns['deleted'], 0)
1132 );
1133 }
1134 if ($this->enablecolumns['starttime']) {
1135 $constraints->add(
1136 $expressionBuilder->lte($tableAlias . '.' . $this->enablecolumns['starttime'], $GLOBALS['EXEC_TIME'])
1137 );
1138 }
1139 if ($this->enablecolumns['endtime']) {
1140 $constraints->add(
1141 $expressionBuilder->orX(
1142 $expressionBuilder->eq($tableAlias . '.' . $this->enablecolumns['endtime'], 0),
1143 $expressionBuilder->gt($tableAlias . '.' . $this->enablecolumns['endtime'], $GLOBALS['EXEC_TIME'])
1144 )
1145 );
1146 }
1147
1148 return $constraints;
1149 }
1150
1151 /**
1152 * This returns the where-clause needed to select the user
1153 * with respect flags like deleted, hidden, starttime, endtime
1154 *
1155 * @return string
1156 * @access private
1157 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
1158 */
1159 protected function user_where_clause()
1160 {
1161 GeneralUtility::logDeprecatedFunction();
1162
1163 $whereClause = '';
1164 if ($this->enablecolumns['rootLevel']) {
1165 $whereClause .= 'AND ' . $this->user_table . '.pid=0 ';
1166 }
1167 if ($this->enablecolumns['disabled']) {
1168 $whereClause .= ' AND ' . $this->user_table . '.' . $this->enablecolumns['disabled'] . '=0';
1169 }
1170 if ($this->enablecolumns['deleted']) {
1171 $whereClause .= ' AND ' . $this->user_table . '.' . $this->enablecolumns['deleted'] . '=0';
1172 }
1173 if ($this->enablecolumns['starttime']) {
1174 $whereClause .= ' AND (' . $this->user_table . '.' . $this->enablecolumns['starttime'] . '<=' . $GLOBALS['EXEC_TIME'] . ')';
1175 }
1176 if ($this->enablecolumns['endtime']) {
1177 $whereClause .= ' AND (' . $this->user_table . '.' . $this->enablecolumns['endtime'] . '=0 OR '
1178 . $this->user_table . '.' . $this->enablecolumns['endtime'] . '>' . $GLOBALS['EXEC_TIME'] . ')';
1179 }
1180 return $whereClause;
1181 }
1182
1183 /**
1184 * This returns the where prepared statement-clause needed to lock a user to the IP address
1185 *
1186 * @return array
1187 * @access private
1188 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
1189 */
1190 protected function ipLockClause()
1191 {
1192 GeneralUtility::logDeprecatedFunction();
1193 $statementClause = array(
1194 'where' => '',
1195 'parameters' => array()
1196 );
1197 if ($this->lockIP) {
1198 $statementClause['where'] = 'AND (
1199 ' . $this->session_table . '.ses_iplock = :ses_iplock
1200 OR ' . $this->session_table . '.ses_iplock=\'[DISABLED]\'
1201 )';
1202 $statementClause['parameters'] = array(
1203 ':ses_iplock' => $this->ipLockClause_remoteIPNumber($this->lockIP)
1204 );
1205 }
1206 return $statementClause;
1207 }
1208
1209 /**
1210 * Returns the IP address to lock to.
1211 * The IP address may be partial based on $parts.
1212 *
1213 * @param int $parts 1-4: Indicates how many parts of the IP address to return. 4 means all, 1 means only first number.
1214 * @return string (Partial) IP address for REMOTE_ADDR
1215 * @access private
1216 */
1217 protected function ipLockClause_remoteIPNumber($parts)
1218 {
1219 $IP = GeneralUtility::getIndpEnv('REMOTE_ADDR');
1220 if ($parts >= 4) {
1221 return $IP;
1222 } else {
1223 $parts = MathUtility::forceIntegerInRange($parts, 1, 3);
1224 $IPparts = explode('.', $IP);
1225 for ($a = 4; $a > $parts; $a--) {
1226 unset($IPparts[$a - 1]);
1227 }
1228 return implode('.', $IPparts);
1229 }
1230 }
1231
1232 /**
1233 * VeriCode returns 10 first chars of a md5 hash of the session cookie AND the encryptionKey from TYPO3_CONF_VARS.
1234 * This code is used as an alternative verification when the JavaScript interface executes cmd's to
1235 * tce_db.php from eg. MSIE 5.0 because the proper referer is not passed with this browser...
1236 *
1237 * @return string
1238 */
1239 public function veriCode()
1240 {
1241 return substr(md5($this->id . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']), 0, 10);
1242 }
1243
1244 /**
1245 * This returns the where-clause needed to lock a user to a hash integer
1246 *
1247 * @return string
1248 * @access private
1249 */
1250 protected function hashLockClause()
1251 {
1252 return 'AND ' . $this->session_table . '.ses_hashlock=' . $this->hashLockClause_getHashInt();
1253 }
1254
1255 /**
1256 * Creates hash integer to lock user to. Depends on configured keywords
1257 *
1258 * @return int Hash integer
1259 * @access private
1260 */
1261 protected function hashLockClause_getHashInt()
1262 {
1263 $hashStr = '';
1264 if (GeneralUtility::inList($this->lockHashKeyWords, 'useragent')) {
1265 $hashStr .= ':' . GeneralUtility::getIndpEnv('HTTP_USER_AGENT');
1266 }
1267 return GeneralUtility::md5int($hashStr);
1268 }
1269
1270 /*************************
1271 *
1272 * Session and Configuration Handling
1273 *
1274 *************************/
1275 /**
1276 * This writes $variable to the user-record. This is a way of providing session-data.
1277 * You can fetch the data again through $this->uc in this class!
1278 * If $variable is not an array, $this->uc is saved!
1279 *
1280 * @param array|string $variable An array you want to store for the user as session data. If $variable is not supplied (is null), the internal variable, ->uc, is stored by default
1281 * @return void
1282 */
1283 public function writeUC($variable = '')
1284 {
1285 if (is_array($this->user) && $this->user[$this->userid_column]) {
1286 if (!is_array($variable)) {
1287 $variable = $this->uc;
1288 }
1289 if ($this->writeDevLog) {
1290 GeneralUtility::devLog(
1291 'writeUC: ' . $this->userid_column . '=' . (int)$this->user[$this->userid_column],
1292 AbstractUserAuthentication::class
1293 );
1294 }
1295 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table)->update(
1296 $this->user_table,
1297 ['uc' => serialize($variable)],
1298 [$this->userid_column => (int)$this->user[$this->userid_column]]
1299 );
1300 }
1301 }
1302
1303 /**
1304 * Sets $theUC as the internal variable ->uc IF $theUC is an array.
1305 * If $theUC is FALSE, the 'uc' content from the ->user array will be unserialized and restored in ->uc
1306 *
1307 * @param mixed $theUC If an array, then set as ->uc, otherwise load from user record
1308 * @return void
1309 */
1310 public function unpack_uc($theUC = '')
1311 {
1312 if (!$theUC && isset($this->user['uc'])) {
1313 $theUC = unserialize($this->user['uc']);
1314 }
1315 if (is_array($theUC)) {
1316 $this->uc = $theUC;
1317 }
1318 }
1319
1320 /**
1321 * Stores data for a module.
1322 * The data is stored with the session id so you can even check upon retrieval
1323 * if the module data is from a previous session or from the current session.
1324 *
1325 * @param string $module Is the name of the module ($MCONF['name'])
1326 * @param mixed $data Is the data you want to store for that module (array, string, ...)
1327 * @param bool|int $noSave If $noSave is set, then the ->uc array (which carries all kinds of user data) is NOT written immediately, but must be written by some subsequent call.
1328 * @return void
1329 */
1330 public function pushModuleData($module, $data, $noSave = 0)
1331 {
1332 $this->uc['moduleData'][$module] = $data;
1333 $this->uc['moduleSessionID'][$module] = $this->id;
1334 if (!$noSave) {
1335 $this->writeUC();
1336 }
1337 }
1338
1339 /**
1340 * Gets module data for a module (from a loaded ->uc array)
1341 *
1342 * @param string $module Is the name of the module ($MCONF['name'])
1343 * @param string $type If $type = 'ses' then module data is returned only if it was stored in the current session, otherwise data from a previous session will be returned (if available).
1344 * @return mixed The module data if available: $this->uc['moduleData'][$module];
1345 */
1346 public function getModuleData($module, $type = '')
1347 {
1348 if ($type != 'ses' || (isset($this->uc['moduleSessionID'][$module]) && $this->uc['moduleSessionID'][$module] == $this->id)) {
1349 return $this->uc['moduleData'][$module];
1350 }
1351 return null;
1352 }
1353
1354 /**
1355 * Returns the session data stored for $key.
1356 * The data will last only for this login session since it is stored in the session table.
1357 *
1358 * @param string $key Pointer to an associative key in the session data array which is stored serialized in the field "ses_data" of the session table.
1359 * @return mixed
1360 */
1361 public function getSessionData($key)
1362 {
1363 $sesDat = unserialize($this->user['ses_data']);
1364 return $sesDat[$key];
1365 }
1366
1367 /**
1368 * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database.
1369 * The data will last only for this login session since it is stored in the session table.
1370 *
1371 * @param string $key Pointer to an associative key in the session data array which is stored serialized in the field "ses_data" of the session table.
1372 * @param mixed $data The variable to store in index $key
1373 * @return void
1374 */
1375 public function setAndSaveSessionData($key, $data)
1376 {
1377 $sesDat = unserialize($this->user['ses_data']);
1378 $sesDat[$key] = $data;
1379 $this->user['ses_data'] = serialize($sesDat);
1380 if ($this->writeDevLog) {
1381 GeneralUtility::devLog('setAndSaveSessionData: ses_id = ' . $this->user['ses_id'], AbstractUserAuthentication::class);
1382 }
1383 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->session_table)->update(
1384 $this->session_table,
1385 ['ses_data' => $this->user['ses_data']],
1386 ['ses_id' => $this->user['ses_id']]
1387 );
1388 }
1389
1390 /*************************
1391 *
1392 * Misc
1393 *
1394 *************************/
1395 /**
1396 * Returns an info array with Login/Logout data submitted by a form or params
1397 *
1398 * @return array
1399 * @internal
1400 */
1401 public function getLoginFormData()
1402 {
1403 $loginData = array();
1404 $loginData['status'] = GeneralUtility::_GP($this->formfield_status);
1405 if ($this->getMethodEnabled) {
1406 $loginData['uname'] = GeneralUtility::_GP($this->formfield_uname);
1407 $loginData['uident'] = GeneralUtility::_GP($this->formfield_uident);
1408 } else {
1409 $loginData['uname'] = GeneralUtility::_POST($this->formfield_uname);
1410 $loginData['uident'] = GeneralUtility::_POST($this->formfield_uident);
1411 }
1412 // Only process the login data if a login is requested
1413 if ($loginData['status'] === 'login') {
1414 $loginData = $this->processLoginData($loginData);
1415 }
1416 $loginData = array_map('trim', $loginData);
1417 return $loginData;
1418 }
1419
1420 /**
1421 * Processes Login data submitted by a form or params depending on the
1422 * passwordTransmissionStrategy
1423 *
1424 * @param array $loginData Login data array
1425 * @param string $passwordTransmissionStrategy Alternative passwordTransmissionStrategy. Used when authentication services wants to override the default.
1426 * @return array
1427 * @internal
1428 */
1429 public function processLoginData($loginData, $passwordTransmissionStrategy = '')
1430 {
1431 $loginSecurityLevel = trim($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['loginSecurityLevel']) ?: 'normal';
1432 $passwordTransmissionStrategy = $passwordTransmissionStrategy ?: $loginSecurityLevel;
1433 if ($this->writeDevLog) {
1434 GeneralUtility::devLog('Login data before processing: ' . GeneralUtility::arrayToLogString($loginData), AbstractUserAuthentication::class);
1435 }
1436 $serviceChain = '';
1437 $subType = 'processLoginData' . $this->loginType;
1438 $authInfo = $this->getAuthInfoArray();
1439 $isLoginDataProcessed = false;
1440 $processedLoginData = $loginData;
1441 while (is_object($serviceObject = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
1442 $serviceChain .= ',' . $serviceObject->getServiceKey();
1443 $serviceObject->initAuth($subType, $loginData, $authInfo, $this);
1444 $serviceResult = $serviceObject->processLoginData($processedLoginData, $passwordTransmissionStrategy);
1445 if (!empty($serviceResult)) {
1446 $isLoginDataProcessed = true;
1447 // If the service returns >=200 then no more processing is needed
1448 if ((int)$serviceResult >= 200) {
1449 unset($serviceObject);
1450 break;
1451 }
1452 }
1453 unset($serviceObject);
1454 }
1455 if ($isLoginDataProcessed) {
1456 $loginData = $processedLoginData;
1457 if ($this->writeDevLog) {
1458 GeneralUtility::devLog('Processed login data: ' . GeneralUtility::arrayToLogString($processedLoginData), AbstractUserAuthentication::class);
1459 }
1460 }
1461 return $loginData;
1462 }
1463
1464 /**
1465 * Returns an info array which provides additional information for auth services
1466 *
1467 * @return array
1468 * @internal
1469 */
1470 public function getAuthInfoArray()
1471 {
1472 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1473 $expressionBuilder = $queryBuilder->expr();
1474 $authInfo = array();
1475 $authInfo['loginType'] = $this->loginType;
1476 $authInfo['refInfo'] = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
1477 $authInfo['HTTP_HOST'] = GeneralUtility::getIndpEnv('HTTP_HOST');
1478 $authInfo['REMOTE_ADDR'] = GeneralUtility::getIndpEnv('REMOTE_ADDR');
1479 $authInfo['REMOTE_HOST'] = GeneralUtility::getIndpEnv('REMOTE_HOST');
1480 $authInfo['showHiddenRecords'] = $this->showHiddenRecords;
1481 // Can be overidden in localconf by SVCONF:
1482 $authInfo['db_user']['table'] = $this->user_table;
1483 $authInfo['db_user']['userid_column'] = $this->userid_column;
1484 $authInfo['db_user']['username_column'] = $this->username_column;
1485 $authInfo['db_user']['userident_column'] = $this->userident_column;
1486 $authInfo['db_user']['usergroup_column'] = $this->usergroup_column;
1487 $authInfo['db_user']['enable_clause'] = $this->userConstraints($expressionBuilder);
1488 if ($this->checkPid && $this->checkPid_value !== null) {
1489 $authInfo['db_user']['checkPidList'] = $this->checkPid_value;
1490 $authInfo['db_user']['check_pid_clause'] = $expressionBuilder->in(
1491 'pid',
1492 GeneralUtility::intExplode(',', $this->checkPid_value)
1493 );
1494 } else {
1495 $authInfo['db_user']['checkPidList'] = '';
1496 $authInfo['db_user']['check_pid_clause'] = '';
1497 }
1498 $authInfo['db_groups']['table'] = $this->usergroup_table;
1499 return $authInfo;
1500 }
1501
1502 /**
1503 * Check the login data with the user record data for builtin login methods
1504 *
1505 * @param array $user User data array
1506 * @param array $loginData Login data array
1507 * @param string $passwordCompareStrategy Alternative passwordCompareStrategy. Used when authentication services wants to override the default.
1508 * @return bool TRUE if login data matched
1509 */
1510 public function compareUident($user, $loginData, $passwordCompareStrategy = '')
1511 {
1512 return (string)$loginData['uident_text'] !== '' && (string)$loginData['uident_text'] === (string)$user[$this->userident_column];
1513 }
1514
1515 /**
1516 * Garbage collector, removing old expired sessions.
1517 *
1518 * @return void
1519 * @internal
1520 */
1521 public function gc()
1522 {
1523 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->session_table);
1524 $query->delete($this->session_table)
1525 ->where(
1526 $query->expr()->lt(
1527 'ses_tstamp',
1528 $query->createNamedParameter((int)($GLOBALS['EXEC_TIME'] - $this->gc_time))
1529 )
1530 )
1531 ->andWhere(
1532 $query->expr()->eq(
1533 'ses_name',
1534 $query->createNamedParameter($this->name)
1535 )
1536 )
1537 ->execute();
1538 }
1539
1540 /**
1541 * DUMMY: Writes to log database table (in some extension classes)
1542 *
1543 * @param int $type denotes which module that has submitted the entry. This is the current list: 1=tce_db; 2=tce_file; 3=system (eg. sys_history save); 4=modules; 254=Personal settings changed; 255=login / out action: 1=login, 2=logout, 3=failed login (+ errorcode 3), 4=failure_warning_email sent
1544 * @param int $action denotes which specific operation that wrote the entry (eg. 'delete', 'upload', 'update' and so on...). Specific for each $type. Also used to trigger update of the interface. (see the log-module for the meaning of each number !!)
1545 * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
1546 * @param int $details_nr The message number. Specific for each $type and $action. in the future this will make it possible to translate errormessages to other languages
1547 * @param string $details Default text that follows the message
1548 * @param array $data Data that follows the log. Might be used to carry special information. If an array the first 5 entries (0-4) will be sprintf'ed the details-text...
1549 * @param string $tablename Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1550 * @param int $recuid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1551 * @param int $recpid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1552 * @return void
1553 */
1554 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)
1555 {
1556 }
1557
1558 /**
1559 * DUMMY: Check login failures (in some extension classes)
1560 *
1561 * @param string $email Email address
1562 * @param int $secondsBack Number of sections back in time to check. This is a kind of limit for how many failures an hour for instance
1563 * @param int $maxFailures Max allowed failures before a warning mail is sent
1564 * @return void
1565 * @ignore
1566 */
1567 public function checkLogFailures($email, $secondsBack, $maxFailures)
1568 {
1569 }
1570
1571 /**
1572 * Raw initialization of the be_user with uid=$uid
1573 * This will circumvent all login procedures and select a be_users record from the
1574 * database and set the content of ->user to the record selected.
1575 * Thus the BE_USER object will appear like if a user was authenticated - however without
1576 * a session id and the fields from the session table of course.
1577 * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause())
1578 *
1579 * @param int $uid The UID of the backend user to set in ->user
1580 * @return void
1581 * @internal
1582 * @see SC_mod_tools_be_user_index::compareUsers(), \TYPO3\CMS\Setup\Controller\SetupModuleController::simulateUser(), freesite_admin::startCreate()
1583 */
1584 public function setBeUserByUid($uid)
1585 {
1586 $this->user = $this->getRawUserByUid($uid);
1587 }
1588
1589 /**
1590 * Raw initialization of the be_user with username=$name
1591 *
1592 * @param string $name The username to look up.
1593 * @return void
1594 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid()
1595 * @internal
1596 */
1597 public function setBeUserByName($name)
1598 {
1599 $this->user = $this->getRawUserByName($name);
1600 }
1601
1602 /**
1603 * Fetching raw user record with uid=$uid
1604 *
1605 * @param int $uid The UID of the backend user to set in ->user
1606 * @return array user record or FALSE
1607 * @internal
1608 */
1609 public function getRawUserByUid($uid)
1610 {
1611 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1612 $query->select('*')
1613 ->from($this->user_table)
1614 ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)))
1615 ->andWhere($this->userConstraints($query->expr()));
1616
1617 return $query->execute()->fetch();
1618 }
1619
1620 /**
1621 * Fetching raw user record with username=$name
1622 *
1623 * @param string $name The username to look up.
1624 * @return array user record or FALSE
1625 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid()
1626 * @internal
1627 */
1628 public function getRawUserByName($name)
1629 {
1630 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1631 $query->select('*')
1632 ->from($this->user_table)
1633 ->where($query->expr()->eq('username', $query->createNamedParameter($name)))
1634 ->andWhere($this->userConstraints($query->expr()));
1635
1636 return $query->execute()->fetch();
1637 }
1638
1639 /*************************
1640 *
1641 * Create/update user - EXPERIMENTAL
1642 *
1643 *************************/
1644 /**
1645 * Get a user from DB by username
1646 * provided for usage from services
1647 *
1648 * @param array $dbUser User db table definition: $this->db_user
1649 * @param string $username user name
1650 * @param string $extraWhere Additional WHERE clause: " AND ...
1651 * @return mixed User array or FALSE
1652 */
1653 public function fetchUserRecord($dbUser, $username, $extraWhere = '')
1654 {
1655 $user = false;
1656 if ($username || $extraWhere) {
1657 $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($dbUser['table']);
1658
1659 $constraints = array_filter([
1660 QueryHelper::stripLogicalOperatorPrefix($dbUser['check_pid_clause']),
1661 QueryHelper::stripLogicalOperatorPrefix($dbUser['enable_clause']),
1662 QueryHelper::stripLogicalOperatorPrefix($extraWhere),
1663 ]);
1664
1665 if (!empty($username)) {
1666 array_unshift(
1667 $constraints,
1668 $query->expr()->eq($dbUser['username_column'], $query->createNamedParameter($username))
1669 );
1670 }
1671
1672 $user = $query->select('*')
1673 ->from($dbUser['table'])
1674 ->where(...$constraints)
1675 ->execute()
1676 ->fetch();
1677 }
1678
1679 return $user;
1680 }
1681
1682 /**
1683 * Get global database connection
1684 * @return DatabaseConnection
1685 */
1686 protected function getDatabaseConnection()
1687 {
1688 return $GLOBALS['TYPO3_DB'];
1689 }
1690 }