1<?php
2
3use Elgg\SystemMessagesService;
4use Elgg\Di\ServiceProvider;
5
6/**
7 * Elgg session management
8 * Functions to manage logins
9 */
10
11/**
12 * Gets Elgg's session object
13 *
14 * @return \ElggSession
15 * @since 1.9
16 */
17function elgg_get_session() {
18	return elgg()->session;
19}
20
21/**
22 * Return the current logged in user, or null if no user is logged in.
23 *
24 * @return \ElggUser|null
25 */
26function elgg_get_logged_in_user_entity() {
27	return elgg()->session->getLoggedInUser();
28}
29
30/**
31 * Return the current logged in user by guid.
32 *
33 * @see elgg_get_logged_in_user_entity()
34 * @return int
35 */
36function elgg_get_logged_in_user_guid() {
37	return elgg()->session->getLoggedInUserGuid();
38}
39
40/**
41 * Returns whether or not the user is currently logged in
42 *
43 * @return bool
44 */
45function elgg_is_logged_in() {
46	return elgg()->session->isLoggedIn();
47}
48
49/**
50 * Returns whether or not the viewer is currently logged in and an admin user.
51 *
52 * @return bool
53 */
54function elgg_is_admin_logged_in() {
55	return elgg()->session->isAdminLoggedIn();
56}
57
58/**
59 * Perform user authentication with a given username and password.
60 *
61 * @warning This returns an error message on failure. Use the identical operator to check
62 * for access: if (true === elgg_authenticate()) { ... }.
63 *
64 *
65 * @see login()
66 *
67 * @param string $username The username
68 * @param string $password The password
69 *
70 * @return true|string True or an error message on failure
71 * @internal
72 */
73function elgg_authenticate($username, $password) {
74	$pam = new \ElggPAM('user');
75	$credentials = ['username' => $username, 'password' => $password];
76	$result = $pam->authenticate($credentials);
77	if (!$result) {
78		return $pam->getFailureMessage();
79	}
80	return true;
81}
82
83/**
84 * Hook into the PAM system which accepts a username and password and attempts to authenticate
85 * it against a known user.
86 *
87 * @param array $credentials Associated array of credentials passed to
88 *                           Elgg's PAM system. This function expects
89 *                           'username' and 'password' (cleartext).
90 *
91 * @return bool
92 * @throws LoginException
93 * @internal
94 */
95function pam_auth_userpass(array $credentials = []) {
96
97	if (!isset($credentials['username']) || !isset($credentials['password'])) {
98		return false;
99	}
100
101	return elgg_call(ELGG_SHOW_DISABLED_ENTITIES, function() use ($credentials) {
102		$user = get_user_by_username($credentials['username']);
103		if (!$user) {
104			throw new \LoginException(_elgg_services()->translator->translate('LoginException:UsernameFailure'));
105		}
106
107		$password_svc = _elgg_services()->passwords;
108		$password = $credentials['password'];
109		$hash = $user->password_hash;
110
111		if (check_rate_limit_exceeded($user->guid)) {
112			throw new \LoginException(_elgg_services()->translator->translate('LoginException:AccountLocked'));
113		}
114
115		if (!$password_svc->verify($password, $hash)) {
116			log_login_failure($user->guid);
117			throw new \LoginException(_elgg_services()->translator->translate('LoginException:PasswordFailure'));
118		}
119
120		if ($password_svc->needsRehash($hash)) {
121			$password_svc->forcePasswordReset($user, $password);
122		}
123
124		return true;
125	});
126}
127
128/**
129 * Log a failed login for $user_guid
130 *
131 * @param int $user_guid User GUID
132 *
133 * @return bool
134 */
135function log_login_failure($user_guid) {
136	return elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function() use ($user_guid) {
137		$user_guid = (int) $user_guid;
138		$user = get_entity($user_guid);
139
140		if (($user_guid) && ($user) && ($user instanceof \ElggUser)) {
141			$fails = (int) $user->getPrivateSetting("login_failures");
142			$fails++;
143
144			$user->setPrivateSetting("login_failures", $fails);
145			$user->setPrivateSetting("login_failure_$fails", time());
146
147			return true;
148		}
149
150		return false;
151	});
152}
153
154/**
155 * Resets the fail login count for $user_guid
156 *
157 * @param int $user_guid User GUID
158 *
159 * @return bool true on success (success = user has no logged failed attempts)
160 */
161function reset_login_failure_count($user_guid) {
162	return elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function() use ($user_guid) {
163		$user_guid = (int) $user_guid;
164
165		$user = get_entity($user_guid);
166
167		if (($user_guid) && ($user) && ($user instanceof \ElggUser)) {
168			$fails = (int) $user->getPrivateSetting("login_failures");
169
170			if ($fails) {
171				for ($n = 1; $n <= $fails; $n++) {
172					$user->removePrivateSetting("login_failure_$n");
173				}
174
175				$user->removePrivateSetting("login_failures");
176
177				return true;
178			}
179
180			// nothing to reset
181			return true;
182		}
183
184		return false;
185	});
186}
187
188/**
189 * Checks if the rate limit of failed logins has been exceeded for $user_guid.
190 *
191 * @param int $user_guid User GUID
192 *
193 * @return bool on exceeded limit.
194 */
195function check_rate_limit_exceeded($user_guid) {
196	return elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES, function() use ($user_guid) {
197		// 5 failures in 5 minutes causes temporary block on logins
198		$limit = 5;
199		$user_guid = (int) $user_guid;
200		$user = get_entity($user_guid);
201
202		if (($user_guid) && ($user) && ($user instanceof \ElggUser)) {
203			$fails = (int) $user->getPrivateSetting("login_failures");
204			if ($fails >= $limit) {
205				$cnt = 0;
206				$time = time();
207				for ($n = $fails; $n > 0; $n--) {
208					$f = $user->getPrivateSetting("login_failure_$n");
209					if ($f > $time - (60 * 5)) {
210						$cnt++;
211					}
212
213					if ($cnt == $limit) {
214						// Limit reached
215						return true;
216					}
217				}
218			}
219		}
220
221		return false;
222	});
223}
224
225/**
226 * Set a cookie, but allow plugins to customize it first.
227 *
228 * To customize all cookies, register for the 'init:cookie', 'all' event.
229 *
230 * @param \ElggCookie $cookie The cookie that is being set
231 * @return bool
232 * @since 1.9
233 */
234function elgg_set_cookie(\ElggCookie $cookie) {
235	return _elgg_services()->responseFactory->setCookie($cookie);
236}
237
238/**
239 * Logs in a specified \ElggUser. For standard registration, use in conjunction
240 * with elgg_authenticate.
241 *
242 * @see elgg_authenticate()
243 *
244 * @param \ElggUser $user       A valid Elgg user object
245 * @param boolean   $persistent Should this be a persistent login?
246 *
247 * @return true or throws exception
248 * @throws LoginException
249 */
250function login(\ElggUser $user, $persistent = false) {
251	if ($user->isBanned()) {
252		throw new \LoginException(elgg_echo('LoginException:BannedUser'));
253	}
254
255	// give plugins a chance to reject the login of this user (no user in session!)
256	if (!elgg_trigger_before_event('login', 'user', $user)) {
257		throw new \LoginException(elgg_echo('LoginException:Unknown'));
258	}
259
260	if (!$user->isEnabled()) {
261		throw new \LoginException(elgg_echo('LoginException:DisabledUser'));
262	}
263
264	// #5933: set logged in user early so code in login event will be able to
265	// use elgg_get_logged_in_user_entity().
266	$session = elgg()->session;
267	$session->setLoggedInUser($user);
268
269	// re-register at least the core language file for users with language other than site default
270	elgg()->translator->registerTranslations(\Elgg\Project\Paths::elgg() . 'languages/');
271
272	// if remember me checked, set cookie with token and store hash(token) for user
273	if ($persistent) {
274		_elgg_services()->persistentLogin->makeLoginPersistent($user);
275	}
276
277	// User's privilege has been elevated, so change the session id (prevents session fixation)
278	$session->migrate();
279
280	$user->setLastLogin();
281	reset_login_failure_count($user->guid);
282
283	elgg_trigger_after_event('login', 'user', $user);
284
285	return true;
286}
287
288/**
289 * Log the current user out
290 *
291 * @return bool
292 */
293function logout() {
294	$session = elgg()->session;
295	$user = $session->getLoggedInUser();
296	if (!$user) {
297		return false;
298	}
299
300	if (!elgg_trigger_before_event('logout', 'user', $user)) {
301		return false;
302	}
303
304	_elgg_services()->persistentLogin->removePersistentLogin();
305
306	// pass along any messages into new session
307	$old_msg = $session->get(SystemMessagesService::SESSION_KEY, []);
308	$session->invalidate();
309	$session->set(SystemMessagesService::SESSION_KEY, $old_msg);
310
311	elgg_trigger_after_event('logout', 'user', $user);
312
313	return true;
314}
315
316/**
317 * Determine which URL the user should be forwarded to upon successful login
318 *
319 * @param \Elgg\Request $request Request object
320 * @param \ElggUser     $user    Logged in user
321 * @return string
322 *
323 * @internal
324 */
325function _elgg_get_login_forward_url(\Elgg\Request $request, \ElggUser $user) {
326
327	$session = elgg_get_session();
328	if ($session->has('last_forward_from')) {
329		$forward_url = $session->get('last_forward_from');
330		$session->remove('last_forward_from');
331		$forward_source = 'last_forward_from';
332	} elseif ($request->getParam('returntoreferer')) {
333		$forward_url = REFERER;
334		$forward_source = 'return_to_referer';
335	} else {
336		// forward to main index page
337		$forward_url = '';
338		$forward_source = null;
339	}
340
341	$params = [
342		'request' => $request,
343		'user' => $user,
344		'source' => $forward_source,
345	];
346
347	return elgg_trigger_plugin_hook('login:forward', 'user', $params, $forward_url);
348
349}
350
351/**
352 * Cleanup expired persistent login tokens from the database
353 *
354 * @param \Elgg\Hook $hook 'cron', 'daily'
355 *
356 * @return void
357 * @since 3.0
358 * @internal
359 */
360function _elgg_session_cleanup_persistent_login(\Elgg\Hook $hook) {
361
362	$time = (int) $hook->getParam('time', time());
363	_elgg_services()->persistentLogin->removeExpiredTokens($time);
364}
365
366/**
367 * Initializes the session and checks for the remember me cookie
368 *
369 * @param ServiceProvider $services Services
370 * @return bool
371 * @throws SecurityException
372 * @internal
373 */
374function _elgg_session_boot(ServiceProvider $services) {
375	$services->timer->begin([__FUNCTION__]);
376
377	$session = $services->session;
378	$session->start();
379
380	// test whether we have a user session
381	if ($session->has('guid')) {
382		/** @var ElggUser $user */
383		$user = $services->entityTable->get($session->get('guid'), 'user');
384		if (!$user) {
385			// OMG user has been deleted.
386			$session->invalidate();
387			forward('');
388		}
389	} else {
390		$user = $services->persistentLogin->bootSession();
391		if ($user) {
392			$services->persistentLogin->updateTokenUsage($user);
393		}
394	}
395
396	if ($user) {
397		$session->setLoggedInUser($user);
398		$user->setLastAction();
399
400		// logout a user with open session who has been banned
401		if ($user->isBanned()) {
402			logout();
403			return false;
404		}
405	}
406
407	$services->timer->end([__FUNCTION__]);
408	return true;
409}
410
411/**
412 * @see \Elgg\Application::loadCore Do not do work here. Just register for events.
413 */
414return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) {
415	register_pam_handler('pam_auth_userpass');
416
417	$hooks->registerHandler('cron', 'daily', '_elgg_session_cleanup_persistent_login');
418};
419