3 * FusionForge session management
5 * Copyright 1999-2001, VA Linux Systems, Inc.
6 * Copyright 2001-2002, 2009, Roland Mas
7 * Copyright 2004-2005, GForge, LLC
8 * Copyright 2013, Franck Villaume - TrivialDev
10 * Thorsten “mirabilos” Glaser <t.glaser@tarent.de>
12 * This file is part of FusionForge. FusionForge is free software;
13 * you can redistribute it and/or modify it under the terms of the
14 * GNU General Public License as published by the Free Software
15 * Foundation; either version 2 of the Licence, or (at your option)
18 * FusionForge is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
23 * You should have received a copy of the GNU General Public License along
24 * with FusionForge; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
28 require_once $gfcommon.'include/account.php';
29 require_once $gfcommon.'include/utils.php';
30 require_once $gfcommon.'include/escapingUtils.php';
33 * A User object if user is logged in
35 * @var constant $G_SESSION
42 $session_ser = getStringFromCookie('session_ser');
45 * session_build_session_token() - Construct session token for the user
47 * @param int User_id of the logged in user
48 * @return string token value
50 function session_build_session_token($user_id) {
54 return session_build_session_cookie($user_id);
57 function session_build_session_cookie($user_id) {
58 $nonce = md5(util_randbytes());
59 if (strlen(forge_get_config('session_key')) < 4) {
60 exit_error('ATTN sysadmin: upgrade your session_key; hint: locate secrets.inc');
62 $session_cookie_data = array(
64 getStringFromServer('REMOTE_ADDR'),
66 getStringFromServer('HTTP_USER_AGENT')
68 $session_cookie = "" . time();
69 foreach ($session_cookie_data as $s) {
70 /* for escaping; this is not really HTML */
71 $session_cookie .= '<' . util_html_encode($s);
73 $session_cookie_hmac = hash_hmac("sha256", $session_cookie,
74 forge_get_config('session_key'), true);
75 $session_serial_cookie = base64_encode($session_cookie) . '!' .
76 base64_encode($session_cookie_hmac);
77 return $session_serial_cookie;
81 * session_get_hash_from_token() - Get hash of session token
83 * This hash can be used as a key to identify session, e.g. in DB.
85 * @param string Value of the session token
88 function session_get_hash_from_token($session_token) {
89 return session_get_session_cookie_hash($session_token);
91 function session_get_session_cookie_hash($session_cookie) {
93 * we cannot just use the HMAC as that may be longer than
94 * the database fields, and this code used to return a
95 * string of the size of an md5(), so just md5 it
97 return md5($session_cookie);
101 * session_check_session_token() - Check that session token passed from user is ok
103 * @param string Value of the session token
104 * @return user_id if token is ok, false otherwise
106 function session_check_session_token($session_token) {
107 if ($session_token == '') {
110 return session_check_session_cookie($session_token);
112 function session_check_session_cookie($session_cookie) {
113 if (!preg_match('#^[A-Za-z0-9+/=]*![A-Za-z0-9+/=]*$#',
116 * does not match basic format, off; recommended by
117 * http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
118 * to protect the below code from malformed strings
123 list($session_cookie, $session_cookie_hmac) = explode('!',
125 $session_cookie = base64_decode($session_cookie);
126 $session_cookie_hmac = base64_decode($session_cookie_hmac);
127 if (hash_hmac("sha256", $session_cookie,
128 forge_get_config('session_key'), true) !== $session_cookie_hmac) {
133 list($time, $user_id, $ip, $nonce, $user_agent) = explode('<', $session_cookie);
134 $user_id = util_unconvert_htmlspecialchars($user_id);
135 $ip = util_unconvert_htmlspecialchars($ip);
136 $user_agent = util_unconvert_htmlspecialchars($user_agent);
138 if (!session_check_ip($ip, getStringFromServer('REMOTE_ADDR'))) {
141 if (trim($user_agent) != getStringFromServer('HTTP_USER_AGENT')) {
144 if ((forge_get_config('session_expire') > 0) &&
145 ($time - time() >= forge_get_config('session_expire'))) {
153 * session_logout() - Log the user off the system.
155 * This function destroys object associated with the current session,
156 * making user "logged out". Deletes both user and session cookies.
158 * @return bool true/false
161 function session_logout() {
162 plugin_hook('close_auth_session');
164 // delete both session and username cookies
165 // NB: cookies must be deleted with the same scope parameters they were set with
167 session_cookie('session_ser', '');
169 RBACEngine::getInstance()->invalidateRoleCaches();
174 * session_login_valid() - Log the user to the system.
176 * High-level function for user login. Check credentials, and if they
177 * are valid, open new session.
179 * @param string $loginname User name
180 * @param string $passwd User password (in clear text)
181 * @param bool|int $allowpending Allow login to non-confirmed user account (only for confirmation of the very account)
182 * @return bool true/false, if false reason is in global $feedback
185 function session_login_valid($loginname, $passwd, $allowpending = 0) {
186 global $feedback, $error_msg, $warning_msg;
188 if (!$loginname || !$passwd) {
189 $warning_msg = _('Missing Password Or User Name');
193 $hook_params = array();
194 $hook_params['loginname'] = $loginname;
195 $hook_params['passwd'] = $passwd;
196 $result = plugin_hook("session_before_login", $hook_params);
198 // Refuse login if not all the plugins are ok.
200 if (!util_ifsetor($feedback)) {
201 $warning_msg = _('Invalid Password Or User Name');
206 return session_login_valid_dbonly($loginname, $passwd, $allowpending);
209 function session_check_credentials_in_database($loginname, $passwd, $allowpending=false) {
210 return session_login_valid_dbonly($loginname, $passwd, $allowpending);
212 function session_login_valid_dbonly($loginname, $passwd, $allowpending) {
213 global $feedback, $userstatus;
215 // Try to get the users from the database using user_id and (MD5) user_pw
216 if (forge_get_config('require_unique_email')) {
217 $res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE (user_name=$1 OR email=$1) AND user_pw=$2',
221 $res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE user_name=$1 AND user_pw=$2',
225 if (!$res || db_numrows($res) < 1) {
226 // No user whose MD5 passwd matches the MD5 of the provided passwd
227 // Selecting by user_name/email only
228 if (forge_get_config('require_unique_email')) {
229 $res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE user_name=$1 OR email=$1',
230 array ($loginname)) ;
232 $res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE user_name=$1',
233 array ($loginname)) ;
235 if (!$res || db_numrows($res) < 1) {
236 // No user by that name
237 $warning_msg = _('Invalid Password Or User Name');
240 // There is a user with the provided user_name/email, but the MD5 passwds do not match
241 // We'll have to try checking the (crypt) unix_pw
242 $usr = db_fetch_array($res);
243 $userstatus = $usr['status'] ;
245 if (crypt ($passwd, $usr['unix_pw']) != $usr['unix_pw']) {
246 // Even the (crypt) unix_pw does not patch
247 // This one has clearly typed a bad passwd
248 $warning_msg = _('Invalid Password Or User Name');
251 // User exists, (crypt) unix_pw matches
252 // Update the (MD5) user_pw and retry authentication
253 // It should work, except for status errors
254 $res = db_query_params ('UPDATE users SET user_pw=$1 WHERE user_id=$2',
257 return session_check_credentials_in_database($loginname, $passwd, $allowpending) ;
260 // If we're here, then the user has typed a password matching the (MD5) user_pw
261 // Let's check whether it also matches the (crypt) unix_pw
262 $usr = db_fetch_array($res);
264 if (crypt ($passwd, $usr['unix_pw']) != $usr['unix_pw']) {
265 // The (crypt) unix_pw does not match
266 if ($usr['unix_pw'] == '') {
267 // Empty unix_pw, we'll take the MD5 as authoritative
268 // Update the (crypt) unix_pw and retry authentication
269 // It should work, except for status errors
270 $res = db_query_params ('UPDATE users SET unix_pw=$1 WHERE user_id=$2',
271 array (account_genunixpw($passwd),
273 return session_check_credentials_in_database($loginname, $passwd, $allowpending) ;
275 // Invalidate (MD5) user_pw, refuse authentication
276 $res = db_query_params ('UPDATE users SET user_pw=$1 WHERE user_id=$2',
277 array ('OUT OF DATE',
279 $warning_msg =_('Invalid Password Or User Name');
284 // Yay. The provided password matches both fields in the database.
285 // Let's check the status of this user
287 // if allowpending (for verify.php) then allow
288 $userstatus = $usr['status'];
289 if ($allowpending && ($usr['status'] == 'P')) {
292 if ($usr['status'] == 'S') {
294 $feedback = _('Account Suspended');
297 if ($usr['status'] == 'P') {
299 $feedback = _('Account Pending');
302 if ($usr['status'] == 'D') {
304 $feedback = _('Account Deleted');
307 if ($usr['status'] != 'A') {
308 //unacceptable account flag
309 $feedback = _('Account Not Active');
313 // create a new session
314 session_set_new(db_result($res, 0, 'user_id'));
321 * session_check_ip() - Check 2 IP addresses for match
323 * This function checks that IP addresses match
325 * IPv4 addresses are allowed to match with some
326 * fuzz factor (within 255.255.0.0 subnet).
328 * For IPv6 addresses, no fuzz is needed since there's
329 * usually no NAT in IPv6.
331 * @param string $oldip The old IP address
332 * @param string $newip The new IP address
333 * @return bool true/false
336 function session_check_ip($oldip, $newip) {
337 if (strstr($oldip, ':')) {
339 if (strstr($newip, ':')) {
340 // New IP is IPv6 too
341 return ($oldip == $newip);
346 if (strstr($newip, ':')) {
350 $eoldip = explode(".", $oldip);
351 $enewip = explode(".", $newip);
353 // require same Class B subnet
354 return (($eoldip[0] == $enewip[0]) && ($eoldip[1] == $enewip[1]));
358 * session_issecure() - Check if current session is secure
362 function session_issecure() {
363 return (strtoupper(getStringFromServer('HTTPS')) == "ON");
367 * session_set_cookie() - Set a session cookie
369 * Set a cookie with default temporal scope of the current browser session
370 * and URL space of the current webserver
372 * @param string $name Name of cookie
373 * @param string $value Value of cookie
374 * @param string $domain Domain scope (default '')
375 * @param int $expiration Expiration time in UNIX seconds (default 0)
377 function session_set_cookie($name, $value, $domain='', $expiration=0) {
378 session_cookie($name, $value, $domain, $expiration);
380 function session_cookie($name, $value, $domain='', $expiration=0) {
381 if (php_sapi_name() == 'cli') {
385 $expiration = time() + $expiration;
387 /* evolvis: force secure (SSL-only) session cookies */
388 //$force_secure = true;
389 /* not (yet?) in FusionForge */
390 $force_secure = false;
391 if ($force_secure && !session_issecure()) {
394 if (PHP_MAJOR_VERSION < 5 || PHP_MINOR_VERSION < 2) {
395 // In PHP < 5.2, setcookie accepts at most 6 parameters
396 setcookie($name, $value, $expiration, '/', $domain, $force_secure);
398 setcookie($name, $value, $expiration, '/', $domain, $force_secure, true);
403 * session_redirect_uri() - Redirect browser
405 * @param string Absolute URI
406 * @return never returns
408 function session_redirect_uri($loc) {
409 util_save_messages();
410 sysdebug_off("Status: 301 Moved Permanently", true, 301);
411 header("Location: ${loc}", true);
412 echo "\nPlease go to ${loc} instead!\n";
417 * session_redirect() - Redirect browser within the site and exit.
419 * @param string $loc Absolute path within the site
421 function session_redirect($loc) {
422 session_redirect_uri(util_make_url($loc));
427 * session_redirect_external() - Redirect browser to a (potentially external) URL
429 * @param string Absolute URL, not necessarily within the site
430 * @return never returns
432 function session_redirect_external($url) {
433 util_save_messages();
434 header('Location: '.$url);
440 * session_redirect404() - Redirect browser to 404 error page
442 * @return never returns
444 function session_redirect404() {
445 global $HTML, $gfwww, $gfcommon;
447 header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
448 require_once $gfwww.'404.php';
453 * session_require() - DEPRECATED Convenience function to easily enforce permissions
455 * Calling page will terminate with error message if current user
460 * @param array $req Associative array specifying criteria
461 * @param string $reason
462 * @return does not return if check is failed
464 function session_require($req, $reason = '') {
465 if (!session_loggedin()) {
466 exit_not_logged_in();
469 $user = user_get_object(user_getid());
470 if (!$user->isActive()) {
472 exit_error(_('Your account is no longer active; you have been disconnected'), '');
475 if (!array_key_exists('group', $req)) {
476 exit_permission_denied($reason, '');
479 $group = group_get_object($req['group']);
480 if (!$group || !is_object($group)) {
482 } elseif ($group->isError()) {
483 exit_error($reason ? $reason : $group->getErrorMessage(), '');
486 $perm =& $group->getPermission();
487 if (!$perm || !is_object($perm) || $perm->isError()) {
488 exit_permission_denied($reason, '');
491 if (isset($req['admin_flags']) && $req['admin_flags']) {
492 if (!$perm->isAdmin()) {
493 exit_permission_denied($reason, '');
496 if (!$perm->isMember()) {
497 exit_permission_denied($reason, '');
503 * session_require_perm() - Convenience function to easily enforce permissions
505 * Calling page will terminate with error message if current user
509 function session_require_perm($section, $reference, $action = NULL, $reason = '') {
510 if (!forge_check_perm($section, $reference, $action)) {
511 exit_permission_denied($reason, $section);
516 * session_require_global_perm() - Convenience function to easily enforce permissions
518 * Calling page will terminate with error message if current user
522 function session_require_global_perm($section, $action=NULL, $reason='') {
523 if (!forge_check_global_perm($section, $action)) {
525 $reason = sprintf(_('Permission denied. The %s administrators will have to grant you permission to view this page.'),
526 forge_get_config('forge_name'));
528 exit_permission_denied($reason, $section);
533 * session_require_login() - Convenience function to easily enforce permissions
535 * Calling page will terminate with error message if current user
539 function session_require_login() {
540 if (!session_loggedin()) {
541 exit_not_logged_in();
546 * session_set_new() - Setup session for the given user
548 * This function sets up SourceForge session for the given user,
549 * making one be "logged in".
551 * @param int $user_id The user ID
553 function session_set_new($user_id) {
554 $token = session_build_session_token($user_id);
556 // set session cookie
558 $cookie = session_build_session_cookie($user_id);
559 // session_cookie("session_ser", $cookie, "", forge_get_config('session_expire'));
560 // $session_ser = $cookie;
562 $res = db_query_params('SELECT count(*) as c FROM user_session
563 WHERE session_hash=$1',
564 array(($shash = session_get_session_cookie_hash($cookie))));
565 if (!$res || db_result($res, 0, 'c') < 1) {
566 db_query_params('INSERT INTO user_session
567 (session_hash,ip_addr,time,user_id)
568 VALUES ($1,$2,$3,$4)',
571 getStringFromServer('REMOTE_ADDR'),
577 // check uniqueness of the session_hash in the database
578 $res = session_getdata($user_id);
581 exit_error(db_error(), '');
582 } elseif (db_numrows($res) < 1) {
583 exit_error(_('Could not fetch user session data'), '');
585 session_set_internal($user_id, $res);
589 function session_set_internal($user_id, $res = false) {
592 $G_SESSION = user_get_object($user_id, $res);
594 $G_SESSION->setLoggedIn(true);
597 RBACEngine::getInstance()->invalidateRoleCaches();
601 * session_set_admin() - Setup session for the admin user
603 * This function sets up a session for the administrator
605 function session_set_admin() {
606 $admins = RBACEngine::getInstance()->getUsersByAllowedAction('forge_admin', -1);
607 if (count($admins) == 0) {
608 exit_error(_('No admin users ?'), '');
611 * Use the user with the lowest numerical user ID.
612 * This is to prevent complaints from real humans
613 * if the system is doing something in their stead
614 * (for example by populate_template_project.php).
615 * Usually, “admin” has the ID 101.
617 $admin_ids = array();
618 foreach ($admins as $admin) {
619 $admin_ids[] = $admin->getID();
622 session_set_new($admin_ids[0]);
626 * Private optimization function for logins - fetches user data, language, and session
629 * @param int $user_id The user ID
633 function session_getdata($user_id) {
634 return db_query_params('SELECT u.*, sl.language_id, sl.name,
635 sl.filename, sl.classname, sl.language_code,
636 t.dirname, t.fullname
637 FROM users u, supported_languages sl, themes t
638 WHERE u.language=sl.language_id
639 AND u.theme_id=t.theme_id
645 * session_set() - Re-initialize session for the logged in user
647 * This function checks that the user is logged in and if so, initialize
648 * internal session environment.
650 function session_set() {
654 // assume bad session_hash and session. If all checks work, then allow
655 // otherwise make new session
659 // pass the session_ser from cookie to the auth plugins
660 // (see AuthBuiltinPlugin::checkAuthSession() or likes)
661 // expect FORGE_AUTH_AUTHORITATIVE_ACCEPT, FORGE_AUTH_AUTHORITATIVE_REJECT or FORGE_AUTH_NOT_AUTHORITATIVE
663 $params['auth_token'] = $session_ser;
664 $params['results'] = array();
665 plugin_hook_by_reference('check_auth_session', $params);
669 foreach ($params['results'] as $p => $r) {
670 if ($r == FORGE_AUTH_AUTHORITATIVE_ACCEPT) {
672 } elseif ($r == FORGE_AUTH_AUTHORITATIVE_REJECT) {
676 if ($seen_yes && !$seen_no) {
677 // see AuthBuiltinPlugin::fetchAuthUser() or likes
678 // expect user object in results
680 $params['results'] = NULL;
681 plugin_hook_by_reference('fetch_authenticated_user', $params);
682 $user = $params['results'];
686 $params['username'] = $user->getUnixName();
687 $params['event'] = 'every-page';
688 plugin_hook('sync_account_info', $params);
690 $user->setLoggedIn(true);
696 // TODO: else... what ?
698 $re = RBACEngine::getInstance();
699 $re->invalidateRoleCaches() ;
703 * Re initializes a session, trusting a non-sufficient plugin only temporarily
705 * The checkAuthSession of the Auth plugin will have to acknowledge the 'sufficient_forced' param in 'check_auth_session' hook
706 * @param string $authpluginname
708 function session_set_for_authplugin($authpluginname) {
712 // assume bad session_hash and session. If all checks work, then allow
713 // otherwise make new session
717 // pass the session_ser from cookie to the auth plugins
718 // (see AuthBuiltinPlugin::checkAuthSession() or likes)
719 // expect FORGE_AUTH_AUTHORITATIVE_ACCEPT, FORGE_AUTH_AUTHORITATIVE_REJECT or FORGE_AUTH_NOT_AUTHORITATIVE
721 $params['sufficient_forced'] = $authpluginname;
723 $params['auth_token'] = $session_ser;
724 $params['results'] = array();
726 plugin_hook_by_reference('check_auth_session', $params);
729 foreach ($params['results'] as $p => $r) {
730 if ($r == FORGE_AUTH_AUTHORITATIVE_ACCEPT) {
737 // see AuthBuiltinPlugin::fetchAuthUser() or likes
738 // expect user object in results
740 $params['results'] = NULL;
742 plugin_hook_by_reference('fetch_authenticated_user', $params);
744 $user = $params['results'];
748 $params['username'] = $user->getUnixName();
749 $params['event'] = 'every-page';
750 plugin_hook('sync_account_info', $params);
752 $user->setLoggedIn(true);echo "user:".$user->getUnixName();
759 // TODO: else... what ?
761 $re = RBACEngine::getInstance();
762 //print_r($re->getGlobalRoles());
763 //print_r($re->getPublicRoles());
764 $re->invalidateRoleCaches() ;
765 //print_r($re->getAvailableRoles());
768 //TODO - this should be generalized and used for pre.php,
769 //SOAP, forum_gateway.php, tracker_gateway.php, etc to
771 function session_continue($sessionKey) {
773 $session_ser = $sessionKey;
775 setup_gettext_from_context();
776 setup_tz_from_context();
777 $LUSER =& session_get_user();
778 if (!is_object($LUSER) || $LUSER->isError()) {
784 function setup_tz_from_context() {
785 $user = session_get_user();
786 if (!is_object($user) || $user->isError()) {
787 $tz = forge_get_config('default_timezone');
789 $tz = $user->getTimeZone();
792 date_default_timezone_set($tz);
796 * session_get_user() - Wrapper function to return the User object for the logged in user.
801 function &session_get_user() {
808 * Get user_id of logged in user
810 function user_getid() {
813 return $G_SESSION->getID();
821 * See if user is logged in
823 function session_loggedin() {
827 return $G_SESSION->isLoggedIn();
835 // c-file-style: "bsd"