<?
// To use:
// include_once "session.php"
// $mysession = new Session;
//
// $mysession->loggedin is TRUE if they have logged in
//
// other attributes are :
// username   - the username they logged in with
// fullname   - whatever full name we know for them
// lastseen   - unix timestamp for their previous page access
// data   - var/array for persistant data, commit by calling the 'save' method

// Session management and authentication mechanism.
class Session
{
    public $loggedin = FALSE;    // Is this a valid logged in user ?
    public $username = '';        // Username
    public $fullname;        // Fullname
    public $email = 0;        // Email waiting?
    public $email_forward;        // Email forwarded?
    public $groups = array();    // users groups
    public $printbalance;        // printer balance
    public $data = '';        // Var/array for session persistant data
    public $token = '';        // session identifier
    public $logintime = '';        // Time which user last gave us credentials
    public $lastseen = '';        // Time of last page request
    private $timeout = 2880;    // Idle timeout limit in minutes (session deleted), 2880 == 48 hours
    private $anonymous_timeout = 120; // Idle timeout limit for sessions which aren't logged in (set lower to stop the session table getting swamped)
    private $secure_timeout = 30;   // Idle timeout limit in minutes (consider session less secure, require reauth for sensitive ops)
    private $max_session_length = 11520; // maximum length of a session, 11520 == 8 days
    private $table = "session";    // session storage table (const)
    private $datahash = '';        // hash of data field

    // Create a new (insecure) session
    private function newsession()
    {
        global $DB, $preferred_hostname, $dbname;

        $token = $this->genSessionID();
        $DB->Execute("insert into {$this->table} (hash, lastseen, ip) values (?,NOW(),?)", array($token, $_SERVER['REMOTE_ADDR']));
        setcookie($dbname . "_session", $token, NULL, "/", $preferred_hostname);

        // delete loggedin cookie if it exists
        setcookie($dbname . "_loggedin", FALSE, time(), "/");
        $this->token = $token;
        return;
    }

    public function isSecure()
    {
        global $DB;
        // is user coming from the IP address they were when they logged in?
        if ($detail['ip'] != $_SERVER['REMOTE_ADDR']) {
            return false;
        } elseif (time() > ($this->logintime + $this->secure_timeout)) {
            // has it been too long since we last asked for credentials?
            return false;
        }

    }

    // Public Object constructor
    function __construct()
    {
        global $DB, $preferred_hostname, $baseurl, $dbname;
        unset($token);

        // if user requests a page via HTTP and claims to be logged in, bump them to HTTPS
        if (!isset($_SERVER['HTTPS']) && (@$_COOKIE[$dbname . '_loggedin'] == "true")) {
            header("HTTP/1.0 307 Temporary redirect");
            header("Location: https://{$preferred_hostname}{$baseurl}{$_SERVER['PATH_INFO']}");
            return;
        }

        // The possible form elements
        $submit = @$_POST['Login'];
        $logout = @$_POST['Logout'];
        $session_user = strtolower(@$_POST['session_user']);
        $session_pass = @$_POST['session_pass'];

        // We havent logged them in yet
        $this->loggedin = FALSE;

        // Time out any old sessions
        $DB->Execute(
            "delete from {$this->table} where lastseen < NOW() - '{$this->timeout} minutes'::reltime " .
            "or logintime < NOW() - '{$this->max_session_length} minutes'::reltime " .
            "or (username IS NULL AND lastseen < NOW() - '{$this->anonymous_timeout} minutes'::reltime)"
        );


        // the possible token data passed from a form
        if (isset($_REQUEST['token']))
            $token = $_REQUEST['token'];

        // Check if we were handed a specific token identifier
        // Otherwise use the value from the cookie we gave out
        if (!isset($token) && isset($_COOKIE[$dbname . '_session']))
            $token = @$_COOKIE[$dbname . '_session'];

        if (isset($token)) $this->token = $token;

        // Log them out if they ask
        if ($logout == "Logout") {
            $this->logout();
            return;
        }

        // Okay, so we still dont have a session id
        // so issue a new one and go back to core
        if (!isset($token)) {
            $this->newsession();
            return;
        }

        // Is this a login attempt ?
        if ($submit != '' && $session_user != '' && $session_pass != '') {
            $this->session_init($session_user, $session_pass);
        }

        // Retrieve session information
        $oldsess = $DB->GetAll("select * from {$this->table} where hash=?", array($this->token));

        if (!$oldsess || count($oldsess) < 1) {
            trigger_error("Session timed out", E_USER_NOTICE);
            $this->newsession();
            return;
        }

        // Extract detail of session for pass-back
        $detail = $oldsess[0];
        $this->data = unserialize((string)$detail['data']);
        $this->lastseen = strtotime($detail['lastseen']);
        $this->logintime = strtotime($detail['logintime']);
        $this->datahash = md5(serialize($this->data));

        // are we actually logged in, fill in more
        if ($detail['username']) {
            // Are we using HTTPS?
            if (!isset($_SERVER['HTTPS'])) {
                trigger_error("Insecure Connection", E_USER_NOTICE);
                $this->loggedin = FALSE;
                return;
            }
            $this->username = $detail['username'];
            $this->fetch_detail($detail['username']);
            $this->loggedin = TRUE;
        }

        // update time stamp
        $DB->Execute("update {$this->table} set lastseen=NOW() where hash=?", array($this->token));

        // check to see if there any messages stored for this user
        if (isset($this->data['messages'])) {
            global $messages;
            if (is_array($messages)) {
                $messages += $this->data['messages'];
            } else {
                $messages = $this->data['messages'];
            }
            unset($this->data['messages']);
            $this->save();
        }
    }

    // generate a string suitable to be used as a session ID
    private function genSessionID()
    {
        global $DB;
        $try = 0;

        $tt = date("D M d H:i:s Y");
        $ip = $_SERVER['REMOTE_ADDR'];
        $nonce = rand(); // this should stop session IDs being (easily) guessable by someone with the algorithm

        do {
            $token = md5("$ip$tt$nonce" . $try++);
            $old = $DB->GetAll("select hash from {$this->table}  where hash=?", array($token));
        } while ($old);

        return $token;
    }

    // Public function: Store the session data away in the database
    public function save()
    {
        global $DB;
        $newhash = md5(serialize($this->data));
        if ($newhash == $this->datahash) {
            // no change in data, dont save
            return;
        }

        $DB->Execute("update {$this->table} set data=? where hash=?", array(serialize($this->data), $this->token));

    }

    // Public function: force a logout of the session
    public function logout()
    {
        global $DB, $dbname;
        $DB->Execute("delete from {$this->table} where hash=?", array($this->token));
        $this->newsession();
        $this->loggedin = FALSE;
        setcookie($dbname . "_loggedin", FALSE, time(), "/");
    }

    // Fill out any extra details we know about the user
    private function fetch_detail($user)
    {
        if (!($ldap = @ldap_connect("ldap://localhost"))) {
            trigger_error("LDAP connect failed", E_USER_ERROR);
            return FALSE;
        }
        $info = $this->ldap_getuser($ldap, $user);
        if (!$info) return FALSE;

        ldap_close($ldap);

        // Check the user's email status
        /*$mailstat = @stat("/var/spool/mail/".$user);
        if ($mailstat[size]>0) {
            if ($mailstat[mtime]>$mailstat[atime]) $this->email = 2;
            else $this->email = 1;
        }*/
        // a sure-fire way to check to see if the user has any unread email
        // the bash script returns 0 for no and 1 for yes, takes one arg, username
        $this->email = shell_exec("../plugins/sucsunreadmail $user");

        if (file_exists($info['homedirectory'][0] . "/.forward")) {
            $forward = file($info['homedirectory'][0] . "/.forward");
            $this->email_forward = preg_replace("/\n/", "", $forward[0]);
        }

        $this->fullname = $info['cn'][0];
        $this->groups = $info['grouplist'];

        $db = new SQLite3('/etc/pykota/pykota.db');
        $result = $db->query("SELECT balance FROM users WHERE username='$user';");
        $this->printbalance = $result->fetchArray()[0];

    }

    /* check using mod_auth_externals helper
    private function check_pass($user, $pass)
    {

        if ($fd === FALSE) {
            $this->errormsg = "Auth system error";
            return FALSE;
        }

        fwrite($fd, "$user\n");
        fwrite($fd, "$pass\n");
        $ret = pclose($fd);
        if ($ret == 0) return TRUE;

        $this->autherror = "u='$user' p='$pass' ret=$ret";
        $this->errormsg = "Invalid Username or Password";
        return FALSE;
    }
    */

    // Get a users full record from ldap
    private function ldap_getuser($ldap, $user)
    {
        // publically bind to find user
        ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
        if (!($bind = @ldap_bind($ldap))) {
            trigger_error("LDAP bind failed", E_USER_ERROR);
            return NULL;
        }
        // find the user
        if (!($search = @ldap_search($ldap, "dc=sucs,dc=org", "(&(uid=$user))"))) {
            trigger_error("LDAP search fail", E_USER_ERROR);
            return NULL;
        }
        $n = ldap_count_entries($ldap, $search);
        if ($n < 1) {
            trigger_error("Username or Password Incorrect", E_USER_WARNING);
            return NULL;
        }
        $info = ldap_get_entries($ldap, $search);

        if (($grpsearch = @ldap_search($ldap, "ou=Group,dc=sucs,dc=org", "memberuid=$user"))) {
            $gn = ldap_count_entries($ldap, $grpsearch);
            $gpile = ldap_get_entries($ldap, $grpsearch);
            $glist = array();
            for ($i = 0; $i < $gn; $i++) {
                $glist[$gpile[$i]['cn'][0]] = $gpile[$i]['gidnumber'][0];
            }
            $info[0]['grouplist'] = $glist;
        }
        return $info[0];
    }

    /* check using ldap directly */
    public function check_pass($user, $pass)
    {
        // Open connection
        if (!($ldap = @ldap_connect("ldap://localhost"))) {
            trigger_error("LDAP connect failed", E_USER_ERROR);
            return FALSE;
        }
        $info = $this->ldap_getuser($ldap, $user);
        if (!$info) return FALSE;

        $real = @ldap_bind($ldap, $info['dn'], $pass);

        ldap_close($ldap);
        if ($real) return TRUE;
        trigger_error("Username or Password Incorrect", E_USER_WARNING);
        return FALSE;

    }

    // Private function: process login form
    private function session_init($user, $pass)
    {
        global $DB, $preferred_hostname, $dbname;
        // Check that this is a valid session start
        // This prevents replay attacks
        $sess = $DB->GetAll("select * from {$this->table} where hash=? and username is NULL", array($this->token));
        if (!$sess || count($sess) < 1) {
            trigger_error("Invalid session, login again.", E_USER_WARNING);
            return;
        }

        if (!$this->check_pass($user, $pass)) return;
        $this->username = $user;

        // the token has likely been used on an insecure connection
        // so generate a new one with the secure flag set
        $oldtoken = $this->token;
        $this->token = $this->genSessionID();
        setcookie($dbname . "_session", $this->token, time() + $this->max_session_length * 60, "/", $preferred_hostname, TRUE);

        // set a cookie as a hint that we're logged in
        // this can be checked for to allow redirecting to SSL to get the secure cookie
        setcookie($dbname . "_loggedin", "true", time() + $this->max_session_length * 60, "/");

        // Update the session, filling in the blanks
        $DB->Execute("update {$this->table} set hash=?, username=?, logintime='NOW()', lastseen='NOW()', ip=? where hash=?",
            array($this->token, $this->username, $_SERVER['REMOTE_ADDR'], $oldtoken));

        // Return back to normal session retrieval
    }

} // end of Class