#!/usr/bin/perl
# -----------------------------------------------------------------------------
# TWC
# Treuwert-Computer GmbH
# Schloßbergring 9
# 79098 Freiburg
#
# Author: Klaus Reger <K.Reger@twc.de>
#
# -----------------------------------------------------------------------------
# Program-Data
# -----------------------------------------------------------------------------
# Project  :
# System   :
# Program  :
# Module   : $RCSfile: Session.pm,v $
# Version  : $Revision: 1.6 $
# Date     : $Date: 2002/02/25 10:50:44 $
# State    : $State: Exp $
#
# Description:
#   This module handles all the things of WWWdb, which have
#   to do with Session
#
# Change-log (See at end of this file)
#
# --- Known Bugs -------------
# please look at the BUGS-File
# --- TODO -------------------
# please look at the TODO-File
# ----------------------------------------------------------------------------

=head1 NAME

WWWdb::Session - Session-Handling for WWWdb

=head1 SYNOPSIS

 use WWWdb::Session;

 $oSession   = Session->new($pDbHandle,
                            ["Id"        => $cSessionKey,]
                            ["IdLength"  => $iKeyLength]);

 $cStateInfo = $oSession->getState($cStateName);
 $cStateInfo = $oSession->setState($cStateName,
                                   $cStateValue);

 $oSession->DESTROY();

 $cId        = $oSession->getId();
 $bIdInDb    = $oSession->getIdAlreadyInDb();
 $iLength    = $oSession->getIdLength();

=cut

# The POD text continues at the end of the file.




use WWWdb::Base;
use Digest::MD5 qw(md5_hex);;

# --------------------------------------------------------------------
# Session:Form
# --------------------------------------------------------------------
package Session;
use vars qw(%hAttribs);

use English;
use strict;

@Session::ISA = qw(Base);

{
    %hAttribs =
      (
       Id            => ["c", "public",  "r", undef],
       IdAlreadyInDb => ["b", "public",  "r", 0],
       IdLength      => ["i", "public",  "r", 16],
       State         => ["h", "private", "r", undef],
       DbHdl         => ["o", "private", "r", undef],
      );
}

# --- constructor ---------------------------------------------------
sub Session::new()
{
   my ($cPkgPI)  = shift;
   my $pDbHdlPI  = shift;
#   my $cIdPI     = shift;
#   my $iIdLengthPI = shift;

   my $cClass   = ref($cPkgPI) || $cPkgPI;

   my $iTries = 0;

   my $hSelfPI   = $cClass->SUPER::new(@_);

   bless ($hSelfPI, $cClass);

   # Re-check Length
   $hSelfPI->_setIdLength($hSelfPI->{"IdLength"});

   $hSelfPI->{"DbHdl"} = $pDbHdlPI;

   # check, if id is already in database
   if($hSelfPI->_isIdValid($hSelfPI->{"Id"}))
   {
      $hSelfPI->{"IdAlreadyInDb"} = 1;
   }
   else
   {
      # Create a new session id
      $hSelfPI->{"Id"} = $hSelfPI->_getIdHash("$PID" . rand());

      while ($iTries++ < 20)
      {
         # FIXME: replace by Db-Class
         last if $hSelfPI->{"DbHdl"}->do
             (sprintf("INSERT INTO wwwdb_state
                       (
                          session_id,
                          key_name,
                          key_value
                       )
                       VALUES
                       (
                          '%s',
                          'last_access',
                          '%s'
                       )",
                      $hSelfPI->{"Id"},
                      scalar(localtime())));

         die("Can't insert into wwwdb_state!")
             if($hSelfPI->{"DbHdl"}->err);

         $hSelfPI->{"Id"} =
             $hSelfPI->_getIdHash($hSelfPI->{"Id"});
      }

      if ($iTries >= 20)
      {
         $hSelfPI->{"Id"} = undef;
      }
   }

   # Read state-info from db
   if($hSelfPI)
   {
      my @lRecord;
      my $pStmtHdl;

      $pStmtHdl = $hSelfPI->{"DbHdl"}->prepare
          (sprintf("SELECT key_name, key_value
                    FROM wwwdb_state
                    WHERE
                       session_id = '%s'",
                   $hSelfPI->{"Id"}));
      $pStmtHdl->execute();

      if($pStmtHdl->err())
      {
         die(sprintf("Database Error occured during %s!" . "<BR>" .
                     "    SQL-Error: (%d) %s" . "<BR>" .
                     "    Statement: %s" . "<BR>",
                     "SELECT FROM wwwdb_state",
                     $pStmtHdl->err,
                     $pStmtHdl->errstr));
      }

      while (@lRecord = $pStmtHdl->fetchrow_array)
      {

         $hSelfPI->setState($lRecord[0], $lRecord[1]);
         $hSelfPI->{"State"}{"$lRecord[0]"}{"iFromDb"}   = 1;
         $hSelfPI->{"State"}{"$lRecord[0]"}{"iWriteCnt"} = 0;
      }

      $pStmtHdl->finish;


   }

   return $hSelfPI;
}


# --- Generate a MD5-Hash (internal) --------------------------------
sub Session::_getIdHash($)
{

   my $hSelfPI   = shift;

   my $cValuePI  = shift;

   my $cResult = "";

   $cResult = substr(Digest::MD5->md5_hex($cValuePI),
                     0,
                     $hSelfPI->getIdLength());

   return $cResult;
}

# --- set the length of the session id (internal) -------------------
sub Session::_setIdLength($)
{
   my $hSelfPI   = shift;
   my $iIdLengthPI = shift;

   if ($iIdLengthPI > 32)
   {
      warn "Max. Length of session-id is 32!\n";
      $iIdLengthPI = 32;
   }

   if ($iIdLengthPI < 4)
   {
      warn "Min. Length of session-id is 4!\n";
      $iIdLengthPI = 4;
   }

   $hSelfPI->{"IdLength"} = $iIdLengthPI;

   return $iIdLengthPI;
}


# --- Check, if this session-id belogs to a valid Session ----------
sub Session::_isIdValid($)
{
   my $hSelfPI   = shift;
   my $cIdPI     = shift;

   my $cResult = 0;
   my $pStmtHdl;

   # length 0 cannot be valid
   if(length($cIdPI) == $hSelfPI->getIdLength())
   {
         # FIXME: replace by Db-Class
      $pStmtHdl = $hSelfPI->{"DbHdl"}->prepare
          (sprintf("SELECT 1
                    FROM wwwdb_state
                    WHERE
                       session_id = '%s'  AND
                       key_name   = 'last_access'",
                   $cIdPI));
      $pStmtHdl->execute();

      if($pStmtHdl->err())
      {
         die(sprintf("Database Error occured during %s!" . "<BR>" .
                     "    SQL-Error: (%d) %s" . "<BR>" .
                     "    Statement: %s" . "<BR>",
                     "SELECT FROM wwwdb_state",
                     $pStmtHdl->err,
                     $pStmtHdl->errstr));
      }

      $cResult = $pStmtHdl->fetchrow_array;

      $pStmtHdl->finish;
   }

   return $cResult;
}



# --- get state-information for this session ------------------------
sub Session::getState($)
{
   my $hSelfPI      = shift;
   my $cStateNamePI = shift;

   my $cResult = undef;

   if(defined $hSelfPI->{"State"}{"$cStateNamePI"})
   {
      $cResult = $hSelfPI->{"State"}{"$cStateNamePI"}{"cValue"};
      $hSelfPI->{"State"}{"$cStateNamePI"}{"iReadCnt"} ++;
   }

#   print STDERR
#       "Session::getState($cStateNamePI) => '$cResult'\n"
#           if $hSelfPI->{"DebugLvl"};

   return $cResult;
}



# --- set state-information for this session ---------------------
sub Session::setState($$)
{
   my $hSelfPI       = shift;
   my $cStateNamePI  = shift;
   my $cStateValuePI = shift;

   my $cResult;

   if(defined $hSelfPI->{"State"}->{"$cStateNamePI"}{"cValue"})
   {
      $hSelfPI->{"State"}->{"$cStateNamePI"}->{"iWriteCnt"} ++;
   }
   else
   {
      $hSelfPI->{"State"}{"$cStateNamePI"}{"iWriteCnt"} = 1;
      $hSelfPI->{"State"}{"$cStateNamePI"}{"iReadCnt"}  = 0;
      $hSelfPI->{"State"}{"$cStateNamePI"}{"iFromDb"}   = 0;
   }

   $hSelfPI->{"State"}->{"$cStateNamePI"}{"cValue"} = $cStateValuePI;

   print STDERR
       "Session::setState($cStateNamePI, '$cStateValuePI')\n"
           if $hSelfPI->{"DebugLvl"};

   return $cResult;
}


sub DESTROY ()
{
   my $hSelfPI       = shift;

   print STDERR "Session: DESTROY()\n";

   $hSelfPI->Dump()
       if $hSelfPI->{"DebugLvl"};

   $hSelfPI->setState("last_access", scalar(localtime()));

   # scan all state-info
   foreach (keys %{$hSelfPI->{"State"}})
   {
      # were the data changed
      if ($hSelfPI->{"State"}->{$_}->{"iWriteCnt"})
      {
         my $cSqlStmt = "";

         # record came from db
         if ($hSelfPI->{"State"}->{$_}->{"iFromDb"})
         {
            if (defined $hSelfPI->{"State"}->{$_}->{"cValue"})
            {
               $cSqlStmt =
                   sprintf("UPDATE wwwdb_state
                             SET
                                 key_value   = '%s',
                                 last_change = 'now'
                             WHERE
                                 session_id = '%s' AND
                                 key_name   = '%s'",
                            $hSelfPI->{"State"}->{$_}{"cValue"},
                            $hSelfPI->{"Id"},
                            $_)
            }
            else
            {

               $cSqlStmt =
                   sprintf("DELETE FROM wwwdb_state
                            WHERE
                                 session_id = '%s' AND
                                 key_name   = '%s'",
                            $hSelfPI->{"Id"},
                            $_);

            }
         }
         # record inserted by application
         else
         {
            if (defined $hSelfPI->{"State"}{$_}{"cValue"})
            {

               $cSqlStmt =
                   sprintf("INSERT INTO wwwdb_state
                             (
                                session_id,
                                key_name,
                                key_value,
                                last_change
                             )
                             VALUES
                             (
                                '%s',
                                '%s',
                                '%s',
                                '%s'
                             )",
                           $hSelfPI->{"Id"},
                           $_,
                           $hSelfPI->{"State"}->{$_}{"cValue"},
                           "now");

            }
         }

         $hSelfPI->{"DbHdl"}->do($cSqlStmt)
             if ($cSqlStmt);

         die(sprintf("Database Error occured!\n" .
                      "    SQL-Error: (%d) %s\n",
                      $hSelfPI->{"DbHdl"}->err,
                      $hSelfPI->{"DbHdl"}->errstr))
             if($hSelfPI->{"DbHdl"}->err());

      }
   }
} # DESTROY


1;


__END__


=head1 DESCRIPTION

=head1 The Session class

Every WWWdb-Session is identyfied by a Session-Id. This is a random
generated hex-string with 16 characters by default. Using this
session-id, the user can continue sessions, he already has started.

For every session WWWdb can store state-information, where the some
information for the user can be placed. Every time this session
gets re-acitvated, the session-parameters for I<this> session
get used.

Internally the state-information is stored in the database in the
table B<wwwdb_state>. When a Session-object gets allocated, the
corresponding data will be fetched from the database. When the
application runs, a cache of this parameters will be used.
The DESTROY-method will write the cache back to the database.

=head2 Note

=over 4

If you want to flush all Objects to the Database during the session
you only have to re-create the session-object. All changed objects
will be stored in the database and a new object with the actual values
gets allocated.

Pleas note, that you will get trouble, when you set up two instances
of the same session-object, because the state-info is cached in the
object!

=back

=head2 Class Methods

=over 4

=item B<new>

 $oSession   = Session->new($pDbHandle,
                            ["Id"       => $cSessionKey,]
                            ["IdLength" => $iKeyLength]);

When you want to create a new session-class, you have to call this
constructor. Because you want to acces the database we need the
DBI-database-handle I<pDbHandle>. The I<cSessionKey> is the
session-key, for which the session should be created.  If you want a
new session-key, you van give either I<""> or I<undef> as
parameter. The third thing we need, is the length of the key.
The default is 16. The minimum value is 4, the maximum is 32.

When everything is ok, the session-data gets fetched, when the session
does already exist, or a new record whith the key I<last_acces>
will be inserted. Then the session-object will be returned.

=item B<getState>

 $cStateInfo = $oSession->Session::getState($cStateName);


This method reads the value of the record with the id I<cStateName>
from the internal hash and gives it back as the result. If the record
does not exist in the database, I<undef> is returned.

=item B<setState>

 $cStateInfo = $oSession->Session::setState($cStateName,
                                            $cStateValue);

setState sets the value of the state-record with the name
I<cStateName> to I<cStateValue>. If the value is I<undef> the
corresponding record will be deleted from the database.

=item B<DESTROY>

This method will be called automatically b Perl, when the object is
going to be destroyed. Then the internal cache must be flushed to the
database. According to the existence of a record in the database
the following actions must be triggered:

  +----------+---------------------------+----------------------+
  | Already  |   What happened with the  |  What happens in the |
  | in db    |   Value of the Record     |  Database            |
  +----------+---------------------------+----------------------+
  | No       |   Set to some Value       |  INSERT record       |
  +----------+---------------------------+----------------------+
  | Yes      |   Set to some Value       |  UPDATE record       |
  +----------+---------------------------+----------------------+
  | No       |   Set to undef            |  - Ignore -          |
  +----------+---------------------------+----------------------+
  | Yes      |   Set to undef            |  DELETE record       |
  +----------+---------------------------+----------------------+

=back

=head2 Class Attributes

=over 4

=item B<Id>

 $cId        = $oSession->getId();

Get the Id of this session. (Read-only)

=item B<IdAlreadyInDb>

 $bIdInDb    = $oSession->getIdAlreadyInDb();

True, when this session was already existing in the
database. (Read-only)

=item B<IdLength>

 $iLength    = $oSession->getIdLength();

The length of the session-id. (Read-only)

=back
