e8ca62e013326460099d08d5e27e9e1e1df436a0
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Authentication / FrontendUserAuthentication.php
1 <?php
2 namespace TYPO3\CMS\Frontend\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 use TYPO3\CMS\Core\Utility\GeneralUtility;
17
18 /**
19 * Extension class for Front End User Authentication.
20 *
21 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
22 * @author René Fritz <r.fritz@colorcube.de>
23 */
24 class FrontendUserAuthentication extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication {
25
26 /**
27 * form field with 0 or 1
28 * 1 = permanent login enabled
29 * 0 = session is valid for a browser session only
30 * @var string
31 */
32 public $formfield_permanent = 'permalogin';
33
34 /**
35 * Lifetime of session data in seconds.
36 * @var int
37 */
38 protected $sessionDataLifetime = 86400;
39
40 /**
41 * @var string
42 */
43 public $usergroup_column = 'usergroup';
44
45 /**
46 * @var string
47 */
48 public $usergroup_table = 'fe_groups';
49
50 /**
51 * @var array
52 */
53 public $groupData = array(
54 'title' => array(),
55 'uid' => array(),
56 'pid' => array()
57 );
58
59 /**
60 * Used to accumulate the TSconfig data of the user
61 * @var array
62 */
63 public $TSdataArray = array();
64
65 /**
66 * @var array
67 */
68 public $userTS = array();
69
70 /**
71 * @var bool
72 */
73 public $userTSUpdated = FALSE;
74
75 /**
76 * Session and user data:
77 * There are two types of data that can be stored: UserData and Session-Data.
78 * Userdata is for the login-user, and session-data for anyone viewing the pages.
79 * 'Keys' are keys in the internal data array of the data.
80 * When you get or set a key in one of the data-spaces (user or session) you decide the type of the variable (not object though)
81 * 'Reserved' keys are:
82 * - 'recs': Array: Used to 'register' records, eg in a shopping basket. Structure: [recs][tablename][record_uid]=number
83 * - sys: Reserved for TypoScript standard code.
84 *
85 * @var array
86 */
87 public $sesData = array();
88
89 /**
90 * @var bool
91 */
92 public $sesData_change = FALSE;
93
94 /**
95 * @var bool
96 */
97 public $userData_change = FALSE;
98
99 /**
100 * @var bool
101 */
102 public $is_permanent;
103
104 /**
105 * @var int|NULL
106 */
107 protected $sessionDataTimestamp = NULL;
108
109 /**
110 * Default constructor.
111 */
112 public function __construct() {
113 parent::__construct();
114
115 // Disable cookie by default, will be activated if saveSessionData() is called,
116 // a user is logging-in or an existing session is found
117 $this->dontSetCookie = TRUE;
118
119 $this->session_table = 'fe_sessions';
120 $this->name = self::getCookieName();
121 $this->get_name = 'ftu';
122 $this->loginType = 'FE';
123 $this->user_table = 'fe_users';
124 $this->username_column = 'username';
125 $this->userident_column = 'password';
126 $this->userid_column = 'uid';
127 $this->lastLogin_column = 'lastlogin';
128 $this->enablecolumns = array(
129 'deleted' => 'deleted',
130 'disabled' => 'disable',
131 'starttime' => 'starttime',
132 'endtime' => 'endtime'
133 );
134 $this->formfield_uname = 'user';
135 $this->formfield_uident = 'pass';
136 $this->formfield_chalvalue = 'challenge';
137 $this->formfield_status = 'logintype';
138 $this->auth_timeout_field = 6000;
139 $this->sendNoCacheHeaders = FALSE;
140 $this->getFallBack = TRUE;
141 $this->getMethodEnabled = TRUE;
142 }
143
144 /**
145 * Returns the configured cookie name
146 *
147 * @return string
148 */
149 static public function getCookieName() {
150 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']);
151 if (empty($configuredCookieName)) {
152 $configuredCookieName = 'fe_typo_user';
153 }
154 return $configuredCookieName;
155 }
156
157 /**
158 * Starts a user session
159 *
160 * @return void
161 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::start()
162 */
163 public function start() {
164 if ((int)$this->auth_timeout_field > 0 && (int)$this->auth_timeout_field < $this->lifetime) {
165 // If server session timeout is non-zero but less than client session timeout: Copy this value instead.
166 $this->auth_timeout_field = $this->lifetime;
167 }
168 $this->sessionDataLifetime = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime'];
169 if ($this->sessionDataLifetime <= 0) {
170 $this->sessionDataLifetime = 86400;
171 }
172 parent::start();
173 }
174
175 /**
176 * Returns a new session record for the current user for insertion into the DB.
177 *
178 * @param array $tempuser
179 * @return array User session record
180 */
181 public function getNewSessionRecord($tempuser) {
182 $insertFields = parent::getNewSessionRecord($tempuser);
183 $insertFields['ses_permanent'] = $this->is_permanent;
184 return $insertFields;
185 }
186
187 /**
188 * Determine whether a session cookie needs to be set (lifetime=0)
189 *
190 * @return bool
191 * @internal
192 */
193 public function isSetSessionCookie() {
194 return ($this->newSessionID || $this->forceSetCookie)
195 && ($this->lifetime == 0 || !isset($this->user['ses_permanent']) || !$this->user['ses_permanent']);
196 }
197
198 /**
199 * Determine whether a non-session cookie needs to be set (lifetime>0)
200 *
201 * @return bool
202 * @internal
203 */
204 public function isRefreshTimeBasedCookie() {
205 return $this->lifetime > 0 && isset($this->user['ses_permanent']) && $this->user['ses_permanent'];
206 }
207
208 /**
209 * Returns an info array with Login/Logout data submitted by a form or params
210 *
211 * @return array
212 * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getLoginFormData()
213 */
214 public function getLoginFormData() {
215 $loginData = parent::getLoginFormData();
216 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
217 if ($this->getMethodEnabled) {
218 $isPermanent = GeneralUtility::_GP($this->formfield_permanent);
219 } else {
220 $isPermanent = GeneralUtility::_POST($this->formfield_permanent);
221 }
222 if (strlen($isPermanent) != 1) {
223 $isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
224 } elseif (!$isPermanent) {
225 // To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
226 // we need to force seeting the session cookie here
227 $this->forceSetCookie = TRUE;
228 }
229 $isPermanent = $isPermanent ? 1 : 0;
230 } elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
231 $isPermanent = 1;
232 } else {
233 $isPermanent = 0;
234 }
235 $loginData['permanent'] = $isPermanent;
236 $this->is_permanent = $isPermanent;
237 return $loginData;
238 }
239
240 /**
241 * Creates a user session record and returns its values.
242 * However, as the FE user cookie is normally not set, this has to be done
243 * before the parent class is doing the rest.
244 *
245 * @param array $tempuser User data array
246 * @return array The session data for the newly created session.
247 */
248 public function createUserSession($tempuser) {
249 // At this point we do not know if we need to set a session or a "permanant" cookie
250 // So we force the cookie to be set after authentication took place, which will
251 // then call setSessionCookie(), which will set a cookie with correct settings.
252 $this->dontSetCookie = FALSE;
253 return parent::createUserSession($tempuser);
254 }
255
256 /**
257 * Will select all fe_groups records that the current fe_user is member of
258 * and which groups are also allowed in the current domain.
259 * It also accumulates the TSconfig for the fe_user/fe_groups in ->TSdataArray
260 *
261 * @return int Returns the number of usergroups for the frontend users (if the internal user record exists and the usergroup field contains a value)
262 */
263 public function fetchGroupData() {
264 $this->TSdataArray = array();
265 $this->userTS = array();
266 $this->userTSUpdated = FALSE;
267 $this->groupData = array(
268 'title' => array(),
269 'uid' => array(),
270 'pid' => array()
271 );
272 // Setting default configuration:
273 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig'];
274 // Get the info data for auth services
275 $authInfo = $this->getAuthInfoArray();
276 if ($this->writeDevLog) {
277 if (is_array($this->user)) {
278 GeneralUtility::devLog('Get usergroups for user: ' . GeneralUtility::arrayToLogString($this->user, array($this->userid_column, $this->username_column)), 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication');
279 } else {
280 GeneralUtility::devLog('Get usergroups for "anonymous" user', 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication');
281 }
282 }
283 $groupDataArr = array();
284 // Use 'auth' service to find the groups for the user
285 $serviceChain = '';
286 $subType = 'getGroups' . $this->loginType;
287 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
288 $serviceChain .= ',' . $serviceObj->getServiceKey();
289 $serviceObj->initAuth($subType, array(), $authInfo, $this);
290 $groupData = $serviceObj->getGroups($this->user, $groupDataArr);
291 if (is_array($groupData) && count($groupData)) {
292 // Keys in $groupData should be unique ids of the groups (like "uid") so this function will override groups.
293 $groupDataArr = GeneralUtility::array_merge($groupDataArr, $groupData);
294 }
295 unset($serviceObj);
296 }
297 if ($this->writeDevLog && $serviceChain) {
298 GeneralUtility::devLog($subType . ' auth services called: ' . $serviceChain, 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication');
299 }
300 if ($this->writeDevLog && !count($groupDataArr)) {
301 GeneralUtility::devLog('No usergroups found by services', 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication');
302 }
303 if ($this->writeDevLog && count($groupDataArr)) {
304 GeneralUtility::devLog(count($groupDataArr) . ' usergroup records found by services', 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication');
305 }
306 // Use 'auth' service to check the usergroups if they are really valid
307 foreach ($groupDataArr as $groupData) {
308 // By default a group is valid
309 $validGroup = TRUE;
310 $serviceChain = '';
311 $subType = 'authGroups' . $this->loginType;
312 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
313 $serviceChain .= ',' . $serviceObj->getServiceKey();
314 $serviceObj->initAuth($subType, array(), $authInfo, $this);
315 if (!$serviceObj->authGroup($this->user, $groupData)) {
316 $validGroup = FALSE;
317 if ($this->writeDevLog) {
318 GeneralUtility::devLog($subType . ' auth service did not auth group: ' . GeneralUtility::arrayToLogString($groupData, 'uid,title'), 'TYPO3\\CMS\\Frontend\\Authentication\\FrontendUserAuthentication', 2);
319 }
320 break;
321 }
322 unset($serviceObj);
323 }
324 unset($serviceObj);
325 if ($validGroup) {
326 $this->groupData['title'][$groupData['uid']] = $groupData['title'];
327 $this->groupData['uid'][$groupData['uid']] = $groupData['uid'];
328 $this->groupData['pid'][$groupData['uid']] = $groupData['pid'];
329 $this->groupData['TSconfig'][$groupData['uid']] = $groupData['TSconfig'];
330 }
331 }
332 if (count($this->groupData) && count($this->groupData['TSconfig'])) {
333 // TSconfig: collect it in the order it was collected
334 foreach ($this->groupData['TSconfig'] as $TSdata) {
335 $this->TSdataArray[] = $TSdata;
336 }
337 $this->TSdataArray[] = $this->user['TSconfig'];
338 // Sort information
339 ksort($this->groupData['title']);
340 ksort($this->groupData['uid']);
341 ksort($this->groupData['pid']);
342 }
343 return count($this->groupData['uid']) ?: 0;
344 }
345
346 /**
347 * Returns the parsed TSconfig for the fe_user
348 * The TSconfig will be cached in $this->userTS.
349 *
350 * @return array TSconfig array for the fe_user
351 */
352 public function getUserTSconf() {
353 if (!$this->userTSUpdated) {
354 // Parsing the user TS (or getting from cache)
355 $this->TSdataArray = \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
356 $userTS = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
357 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
358 $parseObj->parse($userTS);
359 $this->userTS = $parseObj->setup;
360 $this->userTSUpdated = TRUE;
361 }
362 return $this->userTS;
363 }
364
365 /*****************************************
366 *
367 * Session data management functions
368 *
369 ****************************************/
370 /**
371 * Fetches the session data for the user (from the fe_session_data table) based on the ->id of the current user-session.
372 * The session data is restored to $this->sesData
373 * 1/100 calls will also do a garbage collection.
374 *
375 * @return void
376 * @access private
377 * @see storeSessionData()
378 */
379 public function fetchSessionData() {
380 // Gets SesData if any AND if not already selected by session fixation check in ->isExistingSessionRecord()
381 if ($this->id && !count($this->sesData)) {
382 $statement = $this->db->prepare_SELECTquery('*', 'fe_session_data', 'hash = :hash');
383 $statement->execute(array(':hash' => $this->id));
384 if (($sesDataRow = $statement->fetch()) !== FALSE) {
385 $this->sesData = unserialize($sesDataRow['content']);
386 $this->sessionDataTimestamp = $sesDataRow['tstamp'];
387 }
388 $statement->free();
389 }
390 }
391
392 /**
393 * Will write UC and session data.
394 * If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
395 * If the flag $this->sesData_change has been set, the fe_session_data table is updated with the content of $this->sesData
396 * If the $this->sessionDataTimestamp is NULL there was no session record yet, so we need to insert it into the database
397 *
398 * @return void
399 * @see fetchSessionData(), getKey(), setKey()
400 */
401 public function storeSessionData() {
402 // Saves UC and SesData if changed.
403 if ($this->userData_change) {
404 $this->writeUC('');
405 }
406 if ($this->sesData_change && $this->id) {
407 if (empty($this->sesData)) {
408 // Remove session-data
409 $this->removeSessionData();
410 // Remove cookie if not logged in as the session data is removed as well
411 if (empty($this->user['uid']) && $this->isCookieSet()) {
412 $this->removeCookie($this->name);
413 }
414 } elseif ($this->sessionDataTimestamp === NULL) {
415 // Write new session-data
416 $insertFields = array(
417 'hash' => $this->id,
418 'content' => serialize($this->sesData),
419 'tstamp' => $GLOBALS['EXEC_TIME']
420 );
421 $this->sessionDataTimestamp = $GLOBALS['EXEC_TIME'];
422 $this->db->exec_INSERTquery('fe_session_data', $insertFields);
423 // Now set the cookie (= fix the session)
424 $this->setSessionCookie();
425 } else {
426 // Update session data
427 $updateFields = array(
428 'content' => serialize($this->sesData),
429 'tstamp' => $GLOBALS['EXEC_TIME']
430 );
431 $this->sessionDataTimestamp = $GLOBALS['EXEC_TIME'];
432 $this->db->exec_UPDATEquery('fe_session_data', 'hash=' . $this->db->fullQuoteStr($this->id, 'fe_session_data'), $updateFields);
433 }
434 }
435 }
436
437 /**
438 * Removes data of the current session.
439 *
440 * @return void
441 */
442 public function removeSessionData() {
443 $this->db->exec_DELETEquery('fe_session_data', 'hash=' . $this->db->fullQuoteStr($this->id, 'fe_session_data'));
444 }
445
446 /**
447 * Log out current user!
448 * Removes the current session record, sets the internal ->user array to a blank string
449 * Thereby the current user (if any) is effectively logged out!
450 * Additionally the cookie is removed
451 *
452 * @return void
453 */
454 public function logoff() {
455 parent::logoff();
456 // Remove the cookie on log-off, but only if we do not have an anonymous session
457 if (!$this->isExistingSessionRecord($this->id) && $this->isCookieSet()) {
458 $this->removeCookie($this->name);
459 }
460 }
461
462 /**
463 * Executes the garbage collection of session data and session.
464 * The lifetime of session data is defined by $TYPO3_CONF_VARS['FE']['sessionDataLifetime'].
465 *
466 * @return void
467 */
468 public function gc() {
469 $timeoutTimeStamp = (int)($GLOBALS['EXEC_TIME'] - $this->sessionDataLifetime);
470 $this->db->exec_DELETEquery('fe_session_data', 'tstamp < ' . $timeoutTimeStamp);
471 parent::gc();
472 }
473
474 /**
475 * Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
476 * or current-session based (not available when browse is closed, but does not require login)
477 *
478 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
479 * @param string $key Key from the data array to return; The session data (in either case) is an array ($this->uc / $this->sesData) and this value determines which key to return the value for.
480 * @return mixed Returns whatever value there was in the array for the key, $key
481 * @see setKey()
482 */
483 public function getKey($type, $key) {
484 if (!$key) {
485 return NULL;
486 }
487 $value = NULL;
488 switch ($type) {
489 case 'user':
490 $value = $this->uc[$key];
491 break;
492 case 'ses':
493 $value = $this->sesData[$key];
494 break;
495 }
496 return $value;
497 }
498
499 /**
500 * Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
501 * When a value is set the flags $this->userData_change or $this->sesData_change will be set so that the final call to ->storeSessionData() will know if a change has occurred and needs to be saved to the database.
502 * Notice: The key "recs" is already used by the function record_registration() which stores table/uid=value pairs in that key. This is used for the shopping basket among other things.
503 * Notice: Simply calling this function will not save the data to the database! The actual saving is done in storeSessionData() which is called as some of the last things in index_ts.php. So if you exit before this point, nothing gets saved of course! And the solution is to call $GLOBALS['TSFE']->storeSessionData(); before you exit.
504 *
505 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
506 * @param string $key Key from the data array to store incoming data in; The session data (in either case) is an array ($this->uc / $this->sesData) and this value determines in which key the $data value will be stored.
507 * @param mixed $data The data value to store in $key
508 * @return void
509 * @see setKey(), storeSessionData(), record_registration()
510 */
511 public function setKey($type, $key, $data) {
512 if (!$key) {
513 return;
514 }
515 switch ($type) {
516 case 'user':
517 if ($this->user['uid']) {
518 if ($data === NULL) {
519 unset($this->uc[$key]);
520 } else {
521 $this->uc[$key] = $data;
522 }
523 $this->userData_change = TRUE;
524 }
525 break;
526 case 'ses':
527 if ($data === NULL) {
528 unset($this->sesData[$key]);
529 } else {
530 $this->sesData[$key] = $data;
531 }
532 $this->sesData_change = TRUE;
533 break;
534 }
535 }
536
537 /**
538 * Returns the session data stored for $key.
539 * The data will last only for this login session since it is stored in the session table.
540 *
541 * @param string $key
542 * @return mixed
543 */
544 public function getSessionData($key) {
545 return $this->getKey('ses', $key);
546 }
547
548 /**
549 * Saves the tokens so that they can be used by a later incarnation of this class.
550 *
551 * @param string $key
552 * @param mixed $data
553 * @return void
554 */
555 public function setAndSaveSessionData($key, $data) {
556 $this->setKey('ses', $key, $data);
557 $this->storeSessionData();
558 }
559
560 /**
561 * Registration of records/"shopping basket" in session data
562 * This will take the input array, $recs, and merge into the current "recs" array found in the session data.
563 * If a change in the recs storage happens (which it probably does) the function setKey() is called in order to store the array again.
564 *
565 * @param array $recs The data array to merge into/override the current recs values. The $recs array is constructed as [table]][uid] = scalar-value (eg. string/integer).
566 * @param int $maxSizeOfSessionData The maximum size of stored session data. If zero, no limit is applied and even confirmation of cookie session is discarded.
567 * @return void
568 */
569 public function record_registration($recs, $maxSizeOfSessionData = 0) {
570 // Storing value ONLY if there is a confirmed cookie set,
571 // otherwise a shellscript could easily be spamming the fe_sessions table
572 // with bogus content and thus bloat the database
573 if (!$maxSizeOfSessionData || $this->isCookieSet()) {
574 if ($recs['clear_all']) {
575 $this->setKey('ses', 'recs', array());
576 }
577 $change = 0;
578 $recs_array = $this->getKey('ses', 'recs');
579 foreach ($recs as $table => $data) {
580 if (is_array($data)) {
581 foreach ($data as $rec_id => $value) {
582 if ($value != $recs_array[$table][$rec_id]) {
583 $recs_array[$table][$rec_id] = $value;
584 $change = 1;
585 }
586 }
587 }
588 }
589 if ($change && (!$maxSizeOfSessionData || strlen(serialize($recs_array)) < $maxSizeOfSessionData)) {
590 $this->setKey('ses', 'recs', $recs_array);
591 }
592 }
593 }
594
595 /**
596 * Determine whether there's an according session record to a given session_id
597 * in the database. Don't care if session record is still valid or not.
598 *
599 * This calls the parent function but additionally tries to look up the session ID in the "fe_session_data" table.
600 *
601 * @param int $id Claimed Session ID
602 * @return bool Returns TRUE if a corresponding session was found in the database
603 */
604 public function isExistingSessionRecord($id) {
605 // Perform check in parent function
606 $count = parent::isExistingSessionRecord($id);
607 // Check if there are any fe_session_data records for the session ID the client claims to have
608 if ($count == FALSE) {
609 $statement = $this->db->prepare_SELECTquery('content,tstamp', 'fe_session_data', 'hash = :hash');
610 $res = $statement->execute(array(':hash' => $id));
611 if ($res !== FALSE) {
612 if ($sesDataRow = $statement->fetch()) {
613 $count = TRUE;
614 $this->sesData = unserialize($sesDataRow['content']);
615 $this->sessionDataTimestamp = $sesDataRow['tstamp'];
616 }
617 $statement->free();
618 }
619 }
620 return $count;
621 }
622
623 }