Du bist hier: StartPHPKlassen › SessionverwaltungDieses Snippet kommentieren

Sessionverwaltung

Diese Klasse speichert eine beliebig große Session in Cookies. Die SIDs sind SHA-1-Hashes.

<?php
/*
 ******************************************************************************
 * FaraFIN-Webseite                                 SoPra SS 2008 Uni Magdeburg
 * ----------------                                 ---------------------------
 *
 * Dieser Code ist im Rahmen eines Softwareprojekts an der Uni Magdeburg
 * entstanden. Er baut auf PHP 5.2 auf und darf unter den Bedingungen der
 * BSD-Lizenz verwendet werden.
 *
 * Christoph Mewes     <christoph@ymail.com>
 ******************************************************************************
*/

if ( !defined('FaraFIN') ) die('Fremdaufrufe sind verboten.');

/**
 * Sitzungsverwaltung
 *
 * Diese Klasse dient zur Verwaltung der Benutzer-Sitzungen.
 * Sessions werden dabei in der Datenbank abgelegt und
 * auf der Clientseite in Cookies abgelegt. Die
 * Identifikation erfolgt über SHA1-Hashes von möglichst
 * zufälligen Werten.
 *
 * Eine Session hat dabei eine bestimmte Lebensdauer sowie
 * beliebig viele weitere Daten, die der Klasse mittels
 * setValue() und getValue() übermittelt werden können.
 */
class Session extends Object
{
    private $data          = array(); ///< array   die Daten der Session
    private $sid           = "";      ///< string  die aktuellen Session-ID
    private $changes       = false;   ///< bool    true, wenn Änderungen an den Daten vorgenommen wurden
    private $cookieDeleted = false;   ///< bool    true, wenn das Cookie dieser Session gelöscht wurde
    
    
    // ===========================================================================
    // SCHNITTSTELLE
    // ===========================================================================
    

    /**
     * Konstruktor
     *
     * Der Konstruktor versucht, eine vorhandene Session
     * fortzuführen, sofern er eine in der Datenbank findet.
     * Vorher räumt er diese jedoch noch auf, um alte,
     * verwaiste Sessions nicht länger als Datenmüll
     * rumliegen zu lassen.
     *
     * Sessions werden ausschließlich in Cookies gespeichert
     * und haben eine bestimmte Lebensdauer.
     *
     * Sollte eine Session gefunden werden, wird die
     * Ablaufzeit in der Datenbank erhöht und das Cookie
     * erneuert. Wird keine Session gefunden, wird eine neue
     * angelegt und ebenfalls ein Cookie beim Besucher
     * erzeugt.
     *
     * @param int    $lifetime    die Dauer in Sekunden, die ein Cookie gültig sein soll
     * @param string $cookiename  der Name des Cookies
     */
    public function __construct($lifetime, $cookiename)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "is", $lifetime, $cookiename);

        $sql = Registry::get("sql");

        // Erstmal räumen wir die Tabelle auf.
        $this->cleanupSessionTable();

        // Wir suchen uns eine Session-ID...
        $this->sid = self::getSIDFromCookie($cookiename);

        // ... und rufen für die gefundene SID die Daten ab.
        $this->data = $sql->fetch('*', PREFIX.'sessions', "`session_id` = \"".$this->sid."\"");

        // Wenn wir keine Daten haben, erzeugen wir eine leere Session.
        if ( $this->data == false )
        {
            $this->sid = $this->createFreshSession($this->sid, $lifetime);
        }
        else
        {
            // Wenn wir eine gefunden haben, erhöhen wir die Ablaufzeit.
            $expire = time()+$lifetime;
            $sql->query("UPDATE ".PREFIX."sessions SET `expiry` = $expire WHERE `session_id` = \"".$this->sid."\"");

            // Außerdem entfernen wir die Ablaufzeit und SID aus dem $data-Array.
            // Diese Daten sind auch über das Interface NICHT änderbar.

            unset($this->data['session_id']);
            unset($this->data['expiry']);

            $this->dropCookie($lifetime, $cookiename);
        }
    }

    /**
     * Cookie ablegen
     *
     * Diese Methode legt mittels setcookie() ein Cookie
     * beim Besucher ab.
     *
     * @param int    $lifetime  die Dauer in Sekunden, die ein Cookie gültig sein soll
     * @param string $name      der Name des Cookies
     */
    public function dropCookie($lifetime, $name)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "is", $lifetime, $name);
        
        if ( !$this->cookieDeleted )
        {
            setcookie($name, $this->sid, time()+$lifetime, '/');
        }
    }

    /**
     * Cookie löschen
     *
     * Diese Methode setzt das Ende der Gültigkeit eines
     * Cookies auf jetzt - 1000 Sekunden und weist den
     * Browser damit an, das Cookie zu löschen.
     *
     * @param string $name  der Name des Cookies
     */
    public function removeCookie($name)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "s", $name);
        
        $this->cookieDeleted = true;
        setcookie($name, '', time()-1000, '/');
    }

    /**
     * Session löschen
     *
     * Diese Methode löscht die Session aus der Datenbank.
     */
    public function remove()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);

        $sql = Registry::get("sql");

        $sql->query("DELETE FROM ".PREFIX."sessions WHERE `session_id` = \"".$this->sid."\"");
    }

    /**
     * Session speichern
     *
     * Diese Methode speichert alle Änderungen, die
     * zwischenzeitlich an den Sessiondaten vorgenommen
     * wurden, in der Datenbank ab.
     *
     * Die Methode ist unabhängig vom SQL-Layout, nur die
     * Attribute expiry und session_id müssen vorhanden
     * sein, alle anderen Attribute erzeugt es je nach dem,
     * welche sie im Konstruktor vorgefunden hat und welche
     * dem Objekt über setValue() mitgeteilt wurden.
     *
     * @return bool  true, wenn das Aktualisierung erfolgreich verlief, sonst false
     */
    public function update()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);
        
        if ( $this->changes == false )
        {
            return false;
        }

        $sql = Registry::get("sql");

        // Wir speichern die Informationen wieder in der Datenbank.

        $query = "UPDATE ".PREFIX."sessions SET `expiry` = `expiry`";

        foreach ( $this->data as $key => $value )
        {
            if ( strlen($key) >= 2 && substr($key, 0, 2) == "__" )
            {
                continue;
            }

            $query .= ", `$key` = \"$value\"";
        }

        $query .= " WHERE `session_id` = \"".$this->sid."\"";

        return $sql->query($query);
    }

    /**
     * Wert setzen
     *
     * Diese Methode setzt einen Wert für die Session, der
     * dann später beim Aufruf von update() dauerhaft in der
     * Datenbank gespeichert wird. Aus diesem Grund dürfen
     * nur solche Schlüssel verwendet werden, die im
     * SQL-Layout der Session-Tabelle vorkommen, da sonst
     * die Abfrage fehlschlagen und die Session nie
     * gespeichert werden wird.
     *
     * Außerdem dürfen die Schlüssel "session_id" und
     * "expiry" nicht verwendet werden, da diese intern von
     * der Klasse verwaltet werden und niemand von außen an
     * ihnen rumwerkeln sollte.
     *
     * @param  string $key    der Schlüssel, der gesetzt werden soll
     * @param  string $value  der Wert zum Schlüssel
     * @return bool           true, wenn der Wert gesetzt wurde, sonst false
     */
    public function set($key, $value)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "sm", $key, $value);

        if ( $key == "session_id" || $key == "expiry" )
        {
            return false;
        }

        $this->data[$key] = $value;
        $this->changes    = true;

        return true;
    }

    /**
     * Einen Wert auslesen
     *
     * Diese Methode liefert ein Attribut der Session
     * zurück. Wenn der Name nicht gefunden wurde, wird null
     * zurückgegegen.
     *
     * @param  string $key  der Attributsname, dessen Wert zurückgegeben werden soll
     * @return mixed        null, wenn der Wert nicht gefunden wurde, sonst mixed (i.d.R. string)
     */
    public function get($key)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "s", $key);
        
        if ( $key == "sid" ) return $this->sid;
        return isset($this->data[$key]) ? $this->data[$key] : null;
    }

    /**
     * Die Session-ID auslesen
     *
     * Diese Methode liefert die aktuelle Sitzungskennung
     * zurück. Man könnte sie auch mit
     *
     * @code
     * $session->getValue('session_id')
     * @endcode
     *
     * holen.
     *
     * @return string  die Session-ID
     */
    public function getSID()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);
        
        return $this->sid;
    }

    /**
     * Sessions aufräumen
     *
     * Diese Methode löscht alle Session, die sich bereits
     * über ihrem Verfallsdatum (expiry) befinden. Sie wird
     * bei jedem Konstruktor-Aufruf sowie von der
     * Maintenance-Klasse aufgerufen.
     *
     * @return int  die Anzahl der gelöschten Sessions
     */
    public function cleanupSessionTable()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);

        $sql = Registry::get("sql");

        $sql->query("DELETE FROM ".PREFIX."sessions WHERE `expiry` <= ".time());

        return $sql->affectedRows();
    }
    
    public static function isValid($lifetime, $cookiename)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "is", $lifetime, $cookiename);

        $sql = Registry::get("sql");

        // Wir suchen uns eine Session-ID...
        $sid = self::getSIDFromCookie($cookiename);

        // ... und rufen für die gefundene SID die Daten ab.
        return $sql->count(PREFIX.'sessions', "`session_id` = \"$sid\"") == 1;
    }
    
    
    // ===========================================================================
    // PRIVATE METHODEN
    // ===========================================================================
    

    /**
     * SID aus Cookie auslesen
     * 
     * Diese Methode versucht, aus einem Cookie die
     * Session-ID auszulesen. Wird ein Cookie gefunden, dass
     * dem angegebenen Namen entspricht, wird der inhalt
     * geprüft. Ist es keine 40 Zeichen lange Zeichenkette,
     * die nur aus [a-z0-9] besteht, so wird von dieser
     * Methode eine neu genrierte SID zuückgegeben.
     * 
     * Wird kein Cookie gefunden, wird ebenfalls eine neue
     * SID generiert.
     * 
     * @param  string $cookiename  der Name der zu untersuchenden Cookies
     * @return string              die gefundene oder neu generierte SID
     */
    private static function getSIDFromCookie($cookiename)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "s", $cookiename);

        if ( isset($_COOKIE[$cookiename]) )
        {
            $cleaned = self::cleanSID($_COOKIE[$cookiename]);
            return $cleaned ? $cleaned : self::createFreshSID();
        }
        else
        {
            return self::createFreshSID();
        }
    }

    /**
     * SID säubern
     * 
     * Diese Methode überprüft eine mögliche SID auf formale
     * Gültigkeit. Dazu darf sie nur aus alphanumerischen
     * Zeichen bestehen und muss 40 Zeichen lang sein. Ist
     * die mögliche SID wohlgeformt, so wird sie
     * zurückgegeben, ansonsten wird ein leerer String
     * zurückgegeben.
     * 
     * @param  string $sid  die mögliche Session-ID
     * @return string       die gereinigte Session-ID
     */
    private static function cleanSID($sid)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "s", $sid);

        if ( !preg_match("/^[a-z0-9]+$/i", $sid) || strlen($sid) != 40 )
        {
            return '';
        }
        else
        {
            return $sid;
        }
    }

    /**
     * Neue Session-ID generieren
     * 
     * Diese Methode generiert eine neue Session-ID. Dazu
     * verwendet sie einen Zufallswert und die Zeit und
     * bildet davon mittels uniqid und sha1 eine 40 Zeichen
     * lange Zeichenkette.
     * 
     * @return string  eine neue, zufällige Session-ID
     */
    private static function createFreshSID()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);
        
        return sha1(uniqid(rand()).time());
    }

    /**
     * Neue Session erzeugen
     * 
     * Diese Methode erzeugt einen neuen Session-Datensatz
     * in der Datenbank. Dabei versucht sie so lange, den
     * Datensatz einzufügen, bis eine noch nicht verwendete
     * Session-ID gefunden wurde. Nach jedem Misserfolg
     * wird mittels createFreshSID() ein neuer SID-Kandidat
     * erzeugt.
     * 
     * @param  string $sid       die einzufügende SID
     * @param  int    $lifetime  der Zeitpunkt, zu dem die Session ablaufen soll
     * @return string            die (ggf. erneuerte) SID
     */
    private function createFreshSession($sid, $lifetime)
    {
        if(DEBUG) Debug::checkArguments(__METHOD__, "si", $sid, $lifetime);

        $sql    = Registry::get("sql");
        $expire = time()+$lifetime;

        // Wir schauen, ob wir die neue SID einfügen können. Solange das
        // nicht geht, erzeugen wir eine neue. Da sich die SID also im
        // Verlauf der Methode ändern kann, geben wir sie am Ende auch
        // zurück an den Konstruktor.

        $query = "INSERT INTO ".PREFIX."sessions (`session_id`,`expiry`) VALUES ('%SID%',$expire)";

        while ( !$sql->query(str_replace("%SID%", $sid, $query)) )
        {
            $sid = $this->createFreshSID();
        }

        $this->data = $sql->fetch('*', PREFIX.'sessions', "`session_id` = \"".$this->sid."\"");

        unset($this->data['session_id']);
        unset($this->data['expiry']);

        return $sid;
    }
    
    
    // ===========================================================================
    // SIGNATUR
    // ===========================================================================
    

    ///@cond INC_SIGNATURE
    public function signature()
    {
        if(DEBUG) Debug::checkArguments(__METHOD__);
        
        return sprintf("Object<Session> (%s)", $this->sid);
    }
    ///@endcond
}

?>

Kommentar verfassen

Fehler gefunden? Doofer Code? Ein kleines "Danke!"? Hinterlasse einfach einen Kommentar.

(muss sein)
(muss nicht sein, wird nicht angezeigt)

Dein Kommentar wird erst nach einer manuellen Prüfung angezeigt.