<?php
/**
* SASHA :: inc/lib/lib.sessions.php
*
* This contains all of the sessions classes.
*
* @package SASHA
* @copyright (C) 2006-2010 Gordon P. Hemsley
* @license docs/LICENSE BSD License
* @version $Id: lib.sessions.php 84 2010-01-21 01:26:56Z gphemsley $
*/
/**
* Sessions
*
* Base class for Sessions
*
* @todo Standardize whether $secure variables are int or bool.
*
* @package SASHA
* @subpackage Sessions
*/
class Sessions
{
/**
* Sessions->logged_in
*
* @access protected
* @var bool $logged_in Whether the user is logged in (default: FALSE)
*/
protected $logged_in = FALSE;
/**
* Sessions->user_id
*
* @access protected
* @var int $user_id The user ID for the current user (default: USER_ANONYMOUS)
*/
protected $user_id = USER_ANONYMOUS;
/**
* Sessions::__construct()
*
* Acts like a cron job, running preliminary housekeeping before beginning a session.
*/
function __construct()
{
$this->expire_sessions();
$this->get_session();
}
/**
* Sessions::generate_hash()
*
* Generate a session hash based on the given user ID.
*
* @access protected
* @param int $user_id User ID to generate hash for (default: USER_ANONYMOUS)
* @return string Session hash
*/
protected function generate_hash( $user_id = USER_ANONYMOUS )
{
global $Database;
$hash_pieces = array();
// Randomly decide what will come first in the hash array.
$first = rand( 0, 1 );
$sql = 'SELECT username
FROM users
WHERE user_id = ' . (int) $user_id;
$result = $Database->query( $sql );
if( !$Database->has_result( $result ) )
{
$Database->free_result( $result );
return FALSE;
}
while( $row = $Database->fetch_assoc( $result ) )
{
// Compile the hash array using the username and a random number.
// This method could easily be changed without affecting existing hashes.
$hash_pieces[$first] = $row['username'];
$hash_pieces[1 - $first] = rand();
}
$Database->free_result( $result );
return sha1( implode( $hash_pieces ) );
}
/**
* Sessions::validate_hash()
*
* Ensure the given string is a valid 40-character hash.
*
* @access protected
* @param string Session hash to validate
* @return bool Validity of session hash
*/
final protected function validate_hash( $session_hash )
{
return (bool) preg_match( '/[a-z0-9]{40}/', $session_hash );
}
/**
* Sessions::create_session()
*
* Create a new session.
*
* @access protected
* @param int $user_id User ID to create session for (default: USER_ANONYMOUS)
* @param int $secure Whether the new session is a secure session (default: 0)
* @return string Session hash of new session
*/
final protected function create_session( $user_id = USER_ANONYMOUS, $secure = 0 )
{
global $Database;
global $config;
// A secure session requires expiration of older, existing sessions.
if( $secure )
{
$this->expire_sessions( $user_id );
}
$session_hash = $this->generate_hash();
// Check to make sure we actually got a good hash.
if( empty( $session_hash ) || !$this->validate_hash( $session_hash ) )
{
// If we don't have a good hash, try again.
$session_hash = $this->generate_hash();
// If we still don't have a good hash, don't attempt to create the session.
if( empty( $session_hash ) || !$this->validate_hash( $session_hash ) )
{
return FALSE;
}
}
$time_now = time();
$time_then = ( $user_id <= USER_ANONYMOUS ) ? $time_now + DAT_WEEK : $time_now + DAT_MONTH;
$sql = "INSERT INTO sessions ( session_hash, user_id, creation_date, expiration_date, secure )
VALUES ( '$session_hash', $user_id, $time_now, $time_then, $secure )";
if( $result = $Database->query( $sql ) )
{
// setcookie( 'SASHA_session_hash', $session_hash, $time_then, $config['server']['path'], $config['server']['domain'], FALSE, TRUE );
setcookie( 'SASHA_session_hash', $session_hash, $time_then, $config['server']['path'], '', FALSE, TRUE );
$this->user_id = $user_id;
$this->logged_in = ( $user_id > USER_ANONYMOUS ) ? TRUE : FALSE;
return $session_hash;
}
else
{
return FALSE;
}
}
/**
* Sessions::update_session()
*
* Update a given session, usually to change the user ID associated with it.
*
* @todo Allow update of secure session choice.
*
* @access protected
* @param string $session_hash Session hash to update
* @param int $user_id User ID to update session for
* @param bool $secure Whether the new session is a secure session (default: FALSE)
* @return bool Success of session update
*/
final protected function update_session( $session_hash, $user_id, $secure = FALSE )
{
global $Database;
global $config;
if( !$this->validate_hash( $session_hash ) )
{
return FALSE;
}
// A secure session requires expiration of older, existing sessions.
if( $secure )
{
$this->expire_sessions( $user_id );
}
$time_now = time();
$time_then = ( $user_id <= USER_ANONYMOUS ) ? $time_now + DAT_WEEK : $time_now + DAT_MONTH;
$sql = "UPDATE sessions
SET user_id = $user_id, expiration_date = $time_then
WHERE session_hash = '$session_hash'";
if( $result = $Database->query( $sql ) )
{
// setcookie( 'SASHA_session_hash', $session_hash, $time_then, $config['server']['path'], $config['server']['domain'], FALSE, TRUE );
setcookie( 'SASHA_session_hash', $session_hash, $time_then, $config['server']['path'], '', FALSE, TRUE );
$this->user_id = $user_id;
$this->logged_in = ( $user_id > USER_ANONYMOUS ) ? TRUE : FALSE;
return TRUE;
}
else
{
return FALSE;
}
}
/**
* Sessions::get_session()
*
* Get the current session. If one does not exist, create a new one.
*
* @access protected
* @return string Session hash of current session
*/
final protected function get_session()
{
global $Database;
// Check for a valid session hash in a cookie, or else create a new one.
if( exists( $_COOKIE['SASHA_session_hash'] ) && $this->validate_hash( $_COOKIE['SASHA_session_hash'] ) )
{
$session_hash = $_COOKIE['SASHA_session_hash'];
}
else
{
$session_hash = $this->create_session();
}
$sql = "SELECT user_id
FROM sessions
WHERE session_hash = '$session_hash'";
$result = $Database->query( $sql );
if( !$Database->has_result( $result ) )
{
$this->user_id = USER_ANONYMOUS;
$this->logged_in = FALSE;
$Database->free_result( $result );
return $this->create_session();
}
while( $row = $Database->fetch_assoc( $result ) )
{
$this->user_id = (int) $row['user_id'];
$this->logged_in = ( $row['user_id'] > USER_ANONYMOUS ) ? TRUE : FALSE;
}
$Database->free_result( $result );
return $session_hash;
}
/**
* Sessions::expire_sessions()
*
* Expire old sessions.
*
* @access public
* @param int|bool Only expire sessions for given user ID (default: FALSE)
* @return bool Success of session expiration
*/
public function expire_sessions( $user_id = FALSE )
{
global $Database;
// Anonymous users have no right to expire other anonymous users.
if( $user_id === USER_ANONYMOUS )
{
return FALSE;
}
$time_now = time();
if( $user_id )
{
// Delete all sessions for the given user.
$where_sql = 'user_id = ' . $user_id;
}
else
{
// Delete sessions that are expired or really old.
$where_sql = 'expiration_date < ' . $time_now . '
OR ( creation_date + ' . DAT_YEAR . ' ) < ' . $time_now;
}
$sql = 'DELETE FROM sessions
WHERE ' . $where_sql;
if( $result = $Database->query( $sql ) )
{
// TODO: Apparently this doesn't work on MEMORY tables. Unneeded?
//$Database->query( 'OPTIMIZE TABLE sessions' );
return TRUE;
}
else
{
return FALSE;
}
}
/**
* Sessions::get_user()
*
* Get the user ID of the current user.
*
* @access public
* @return int User ID of current user.
*/
public function get_user()
{
return $this->user_id;
}
/**
* Sessions::log_in()
*
* Perform the required processing to consider the user logged in.
*
* @access public
* @param string $username Username of user attempting to log in
* @param string $password Password of user attempting to log in
* @param int|bool $secure Log in using a secure session
* @return bool Success of login
*/
public function log_in( $username, $password, $secure )
{
global $Database, $SASHA;
$sql = "SELECT user_id, username, password
FROM users
WHERE username = '" . $Database->escape( strtolower( $username ) ) . "'";
$result = $Database->query( $sql );
if( !$Database->has_result( $result ) )
{
$Database->free_result( $result );
return FALSE;
}
while( $row = $Database->fetch_assoc( $result ) )
{
// Hash of given password must match password hash in user table.
if( sha1( $password ) == $row['password'] )
{
// The current user's session must be updated to match their new user ID.
if( $this->update_session( $this->get_session(), $row['user_id'], (bool) $secure ) )
{
$this->logged_in = TRUE;
}
}
}
$Database->free_result( $result );
return $this->logged_in;
}
/**
* Sessions::log_out()
*
* Perform the required processing to consider the user logged out.
*
* @access public
* @return bool Success of logout
*/
public function log_out()
{
// Reset current session to belong to an anonymous user.
if( $this->update_session( $this->get_session(), USER_ANONYMOUS ) )
{
$this->logged_in = FALSE;
$this->user_id = USER_ANONYMOUS;
return TRUE;
}
else
{
return FALSE;
}
}
/**
* Sessions::print_login_form()
*
* Perform the required processing to consider the user logged in.
*
* @access public
* @param string|bool $username Previous value of 'username' field (default: FALSE)
* @param string|bool $password Previous value of 'password' field (default: FALSE)
* @param int|bool $secure Previous value of 'secure' field (default: FALSE)
* @return void Prints login form
*/
public function print_login_form( $username = FALSE, $password = FALSE, $secure = FALSE )
{
global $SASHA;
$redirect = '';
// Ensure that we (safely) direct the user back to whatever page they were on.
// But only if they're attempting to use a protected page.
// Otherwise, they probably don't want to go back there.
if( exists( $_REQUEST['redirect'] ) && ( substr( $_REQUEST['redirect'], 0, 1 ) != '.' ) )
{
$redirect = '?redirect=' . $_REQUEST['redirect'];
}
elseif( defined( 'THIS_FILE' ) && !defined( 'OVERRIDE_LOGIN' ) )
{
$redirect = '?redirect=' . THIS_FILE;
}
$form_data = array(
array(
'type' => 'header',
'name' => 'login-header',
'label' => 'Log In',
'data' => array(
'level' => 2
)
),
array(
'type' => 'text',
'name' => 'username',
'label' => 'Username',
'data' => array(
'size' => 25,
'maxlength' => 255,
'value' => $username
)
),
array(
'type' => 'password',
'name' => 'password',
'label' => 'Password',
'data' => array(
'size' => 25,
'maxlength' => 255,
'value' => $password
)
),
array(
'type' => 'submit',
'name' => 'submit',
'data' => array(
'value' => 'Log In'
)
)
);
print_message( NULL, 'SASHA requires you to be authenticated in order to access your personal data. Please use the form below to log in. Cookies are required beyond this point.' );
$SASHA->create_form( 'login-form', ROOT . 'login.php' . htmlentities( $redirect, ENT_QUOTES, 'UTF-8' ), $form_data );
}
/**
* Sessions::is_logged_in()
*
* Get logged-in status of the current user.
*
* @access public
* @return bool Is the user logged in?
*/
public function is_logged_in()
{
return $this->logged_in;
}
}
?>