package Session::DB;
#use base 'Tie::Hash';
use strict;
use warnings;
sub new {
my $this = shift;
my $class = ref($this) || $this;
my %arg = @_;
my $self = \%arg;
bless $self, $class;
return $self;
}
# TIEHASH classname, LIST
sub TIEHASH {
my $class = shift;
my $self = new $class, @_;
$self->{path} ||= '/tmp';
die "Session path exists, but is not a directory.\n" if -e $self->{path} && !-d _;
$self->{data} = {};
return $self;
}
# FETCH this, key
sub FETCH {
my $self = shift;
my $key = shift;
local *FH;
my $file = $self->{path}."/".$key.".ses";
if(!open(FH,$file)) {
die "Failed to open '" .$file. "': $!\n";
}
my $data = join '', <FH>;
$self->{data}{$key} = $data; #Storable::thaw($data);
close FH;
chmod 0766, $file;
return $self->{data}{$key};
}
# STORE this, key, value
sub STORE {
my $self = shift;
my $key = shift;
my $val = shift;
local *FH;
if(!open(FH,">".$self->{path}."/".$key.".ses")) {
die "Failed to open '" .$self->{path}."/".$key. ".ses': $!\n";
}
my $data = $val; #Storable::freeze($val);
$self->{data}{$key} = $data;
print FH $data;
close FH;
}
# DELETE this, key
sub DELETE {
my $self = shift;
my $key = shift;
my $file = $self->{path}."/".$key.".ses";
if(-e $file && !-f _) {
die "Attempt to delete session failed, '$file' is not a plain file.";
}
unlink $file;
delete $self->{data}{$key};
}
# CLEAR this
sub CLEAR {
my $self = shift;
local *DIR;
opendir DIR, $self->{path} or die "failed to open session path for listing: $!\n";
for my $ent (readdir DIR) {
if($ent =~ /^(.*).ses$/) {
delete $self->{data}{$1};
unlink $self->{path}."/".$ent;
}
}
closedir DIR;
}
# EXISTS this, key
sub EXISTS {
my $self = shift;
my $key = shift;
my $file = $self->{path}."/".$key.".ses";
if(-e $file && -f _) {
return 1;
}
return 0;
}
sub refresh_keys {
my $self = shift;
opendir DIR, $self->{path} or die "failed to open session path for listing: $!\n";
$self->{list} = [];
for my $ent (readdir DIR) {
next if $ent =~ /^\./;
if($ent =~ /^(.*).ses$/) {
push @{$self->{list}}, $1;
}
}
closedir DIR;
}
# FIRSTKEY this
sub FIRSTKEY {
my $self = shift;
refresh_keys($self);
$self->{li} = 1;
return $self->{list}[0];
}
# NEXTKEY this, lastkey
sub NEXTKEY {
my $self = shift;
#my $lkey = shift; not needed
my $ret = $self->{list}[$self->{li}];
++$self->{li};
return $ret;
}
# SCALAR this
sub SCALAR {
my $self = shift;
return scalar @{$self->{list}};
}
# DESTROY this
sub DESTROY {
my $self = shift;
# anything needed here?
}
# UNTIE this
sub UNTIE {
my $self = shift;
# anything needed here?
}
package Session;
#use warnings;
#use strict;
use CGI::Carp qw/fatalsToBrowser/;
use CGI::Cookie;
use Fcntl; # For O_RDWR, O_CREAT, etc.
use Digest::MD5 qw/md5_hex/;
use Storable qw/freeze thaw/;
$Storable::forgive_me = 1;
our $NAME = 'SESSIONID';
our %sessions;
sub exists : method { return exists $sessions{shift()->{'id'}}; }
sub thaw_session {
my $self = shift;
# print "Thawed: " . $self->{'db'} . "<br>\n";
$self->{'db'} = thaw($sessions{$self->{'id'}});
}
sub freeze_session {
my $self = shift;
$sessions{$self->{'id'}} = freeze($self->{'db'});
# print "Froze: " . length($sessions{$self->id()}) . " bytes<br>\n";
}
sub name {
my $cls = shift;
my $ref = ref($cls) eq 'Session';
my $val = shift;
my $old = $ref ? ( exists $cls->{'NAME'} ? $cls->{'NAME'} : $NAME ) : $NAME;
if(defined $val) {
if($ref) {
$cls->{'NAME'} = $val;
} else {
$NAME = $val;
}
}
return $old;
}
sub new {
my $this = shift;
my $class = ref($this) || $this;
my %arg = @_;
my $self = \%arg;
bless $self, $class;
my %cookies = get_cookies();
$self->{'cookies'} = \%cookies;
if(!exists $cookies{$self->name()}) {
$self->{'id'} = gen_id();
} else {
$self->{'id'} = $cookies{$self->name()};
}
my $path = $self->{'BASE_PATH'} . "/sessions/";
if(!-e $path) {
mkdir $path;
}
if(!tied(%sessions)) {
tie(%sessions, 'Session::DB', path => $path) or die "failed to tie session db: '" . $path . "': $!\n";
if(!tied(%sessions)) {
print "OMFG!<br>\n";
}
}
# use Data::Dumper;
# print Dumper(\%sessions) . "\n";
if(exists $sessions{$self->{'id'}}) {
# print "THAW BIATCH\n";
$self->{'db'} = thaw($sessions{$self->{'id'}}); #thaw_session();
} else {
$self->{'db'} = {
_META => {
created => time(),
accessed => time(),
expires => _time_alias('+1y'),
'keys' => { },
},
};
$sessions{$self->{'id'}} = Storable::freeze($self->{'db'});
}
untie(%sessions);
if(time() > $self->ctime() + $self->etime()) {
# print "sess: initial clear?<br>\n";
$self->{'db'} = {};
} else {
$self->_expire_keys();
}
$self->{'db'}{'_META'}{'accessed'} = time();
return $self;
}
sub clear {
my $self = shift;
my $todo = shift;
if(ref($todo) eq 'ARRAY') {
for(@$todo) {
next if /^_META$/;
delete $self->{'db'}{$_};
delete $self->{'db'}{'_META'}{'keys'}{$_} if exists $self->{'db'}{'_META'}{'keys'}{$_};
}
} else {
delete $self->{'db'}{$todo};
delete $self->{'db'}{'_META'}{'keys'}{$todo} if exists $self->{'db'}{'_META'}{'keys'}{$todo};
}
}
sub cookie {
my $self = shift;
return new CGI::Cookie(-name => $self->name(), -value => $self->id(), -expires => "+1y")
}
sub gen_id()
{
my $id;
for(1..40) { $id .= chr(rand(time)&255); }
return md5_hex($id);
}
sub key_exists {
my $self = shift;
my $key = shift;
return exists $self->{'db'}{$key};
}
sub key {
my $self = shift;
# return keys %{$self->{'db'}} if wantarray;
my $key = shift;
my $val = shift;
# print "key: '$key' => '$val' :: " . join(', ', caller()) . "<br>\n";
if(!exists $self->{'db'}{$key} && !defined $val) {
return undef;
}
if(!exists $self->{'db'}{'_META'}{'keys'}{$key}) {
# print "add meta: '$key'<br>\n";
$self->{'db'}{'_META'}{'keys'}{$key} = { };
}
$self->{'db'}{'_META'}{'keys'}{$key}{'accessed'} = time();
my $prev = exists $self->{'db'}{$key} ? $self->{'db'}{$key} : undef;
if(exists $self->{'db'}{'_META'}{'keys'}{$key}{'expires'} && $self->{'db'}{'_META'}{'keys'}{$key}{'expires'} + $self->{'db'}{'_META'}{'keys'}{$key}{'created'} <= time()) {
delete $self->{'db'}{'_META'}{'keys'}{$key};
delete $self->{'db'}{$key};
return undef;
}
if(defined $val) {
$self->{'db'}{$key} = $val;
# print "setting $key: $val<br>\n";
} else {
# print "getting $key: $prev<br>\n";
}
return $prev;
}
sub ref { return shift->{'db'}; }
sub id { return shift->{'id'}; }
sub atime { return shift->{'db'}{'_META'}{'accessed'}; }
sub etime { return shift->{'db'}{'_META'}{'expires'}; }
sub ctime { return shift->{'db'}{'_META'}{'created'}; }
sub expire {
my $self = shift;
my $oexp = $self->{'db'}{'_META'}{'expires'};
$self->{'db'}{'_META'}{'expires'} = _time_alias($_[0]) if defined $_[0];
return $oexp;
}
sub expire_key {
my $self = shift;
my $key = shift;
my $oexp = ((exists $self->{'db'}{'_META'}{'keys'}{$key}) ? $self->{'db'}{'_META'}{'keys'}{$key}{'expires'} : undef);
if($_[0] == -1) {
delete $self->{'db'}{'_META'}{'keys'}{$key}{'expires'};
} else {
$self->{'db'}{'_META'}{'keys'}{$key}{'created'} = time();
$self->{'db'}{'_META'}{'keys'}{$key}{'expires'} = _time_alias($_[0]) if defined $_[0];
}
return $oexp;
}
sub _expire_keys {
my $self = shift;
for my $key (keys %{$self->{'db'}{'_META'}{'keys'}}) {
next unless exists $self->{'db'}{'_META'}{'keys'}{$key}{'expires'};
delete $self->{'db'}{'_META'}{'keys'}{$key} if(
$self->atime() >= ($self->{'db'}{'_META'}{'keys'}{$key}{'created'} + $self->{'db'}{'_META'}{'keys'}{$key}{'expires'}));
}
}
# internal
sub get_cookies {
return map { my ($k,$v) = split '=', $_; $k => $v } split(';', $ENV{"HTTP_COOKIE"});
}
DESTROY {
my $self = shift;
if(time() > $self->etime() + $self->ctime()) {
$self->{'db'} = undef;
# print "deleted session: " . $self->id() . "<br>\n";
}
#use Data::Dumper;
#print Dumper($self->{'db'}) . "\n";
# print "saving session: " . $self->id() . "<br>\n";
my $path = $self->{'BASE_PATH'} . "/sessions/";
if(!-e $path) {
mkdir $path;
}
tie(%sessions, 'Session::DB', path => $path) or die "failed to tie session db: $!\n";
if(defined $self->{'db'}) {
my $ref = $self->{'db'};
$sessions{$self->{'id'}} = Storable::freeze($ref);
} else {
delete $sessions{$self->{'id'}};
}
untie %sessions;
# print "saved.<br>\n"
}
# internal
# borowed from CGI::Session
sub _time_alias {
my ($str) = @_;
# If $str consists of just digits, return them as they are
if ( $str =~ m/^\d+$/ ) {
return $str;
}
my %time_map = (
s => 1,
m => 60,
h => 3600,
d => 86400,
w => 604800,
M => 2592000,
y => 31536000
);
my ($koef, $d) = $str =~ m/^([+-]?\d+)(\w)$/;
if ( defined($koef) && defined($d) ) {
return $koef * $time_map{$d};
}
}
1;