|
Fall Semester 2004 |
DBI.pm and
MySQL. Now that we posted the long notes about the Hangman example we want to make sure that you read them. But perhaps you won't or perhaps you will find them too difficult, so complicated that although you read them, it really feels like you're not. So I would like to work out a simplified example, in which the very essence of these programs will be looked at, and in a known context, to make it even easier to focus on the the new parts.
So let's work out the traditional calculator program (from
homework two) within this new approach (illustrated by the second Hangman
program and set of notes) in which state is being kept in a database, on
the server-side.
Our first step would be to worry about session management. If we do it as in the program illustrated in Lecture Notes Ten then to illustrate only the process of identifying, extracting, and generating sessions one could write the following program:
#!/usr/bin/perl
#--------------------(these are the modules we will be using)------------------
use CGI;
use MD5;
#--------------------(these are some constants)--------------------------------
$ID_LENGTH = 8; # length of session_id
#--------------------(let's get ready to process)------------------------------
$q = new CGI;
my $session_id = &get_session_id(); # check get_session_id below though,
# if we don't have one we create one
# and then we redirect to this script
# with the session added as path info
print $q->header(), $q->start_html(); # redirection is done at the level
# of headers, and that's why this
# script is somewhat difficult to
# debug, and why the beginning of
# the HTML page comes only here
print "Your session ID is: $session_id"; # note that we need to devise a
# mechanism to keep the id's unique
print $q->end_html(); # end of script, helper procedures are defined below
#------------------------------------------------------(sub get_session_id)---
sub get_session_id { # this subroutine tries to extract an id from the
# path information, and if it does not find one, or
# the format of the one it finds is not correct, it
# generates a session id with the right format and
# redirects the browser to the same script with the
# session id appended to the path
my ($id) = $q->path_info() =~ m:^/([a-h0-9]{$ID_LENGTH}):; # extract id
return $id if $id;
$id = &generate_id(); # if we reach this stage we didn't find
# a (valid) id, so we generate a one now
print $q->redirect($q->script_name() . "/$id"); # and we call ourselves
# right away with the id
# as added path info
exit 0;
}
#-------------------------------------------------------(sub generate_id)---
sub generate_id {
$SECRET = "some secret phrase";
my $id = hash($SECRET . rand()); # note that hash is defined below
}
#--------------------------------------------------------------(sub hash)---
sub hash {
my $value = shift; # take the first argument and use it in hexhash
return substr(MD5->hexhash($value), 0, $ID_LENGTH);
}
One can try this script here.
If you try it, what do you notice:
What we need now is to provide the capability to:
We will be using this table to store the sessions and the state:
mysql> use a348
Database changed
mysql> create table dgerman_accumulator (
-> session_id char(8) primary key,
-> acc int,
-> modified timestamp
-> );
Query OK, 0 rows affected (0.02 sec)
To keep a long story short, the program looks like this:
#!/usr/bin/perl
use CGI;
use DBI;
use MD5;
$DB = "DBI:mysql:a348"; $username = "a348"; $password = "a348AG";
$DB_TABLE = "dgerman_accumulator"; $SECRET = "something secret";
$EXPIRE = 30 * 60 * 60 * 24; # one month
$MAX_TRIES = 10; $ID_LENGTH = 8; $q = new CGI;
$DBH = DBI->connect($DB, $username, $password, { PrintError => 0 }) ||
die "Couldn't open database: ", $DBI::errstr;
my ($session_id) = &get_session_id();
my $acc = &get_state($session_id);
# note: no need to initialize if it's not found
$acc = &calculate($acc,
$q->param('fun'),
$q->param('arg'));
&save_state($acc, $session_id);
print $q->header, $q->start_html;
&status($acc);
&show_form();
print $q->end_html;
$DBH->disconnect;
#--------------------------------(end of main program)------
sub show_form {
print $q->start_form(),
"Type an argument: ",
$q->textfield(-name=>'arg',
-value=>'',
-override=>1),
$q->p(),
"Then please choose a function: ",
$q->popup_menu(
-name => 'fun',
-values => ['non', 'add', 'sub'],
-labels => { 'non' => 'Click me!',
'add' => 'Deposit',
'sub' => 'Withdraw'
},
-default => 'non'
),
$q->p(),
"When done please press ",
$q->submit(-value=>'Proceed'); # perhaps add $q->end_form(); here?
}
#--------------------------------(this was our basic form)---
sub get_session_id {
&expire_old_sessions();
my ($id) = $q->path_info =~ m:^/([a-h0-9]{$ID_LENGTH}):o;
return $id if $id and &check_id($id);
my $session_id = &generate_id;
die "Couldn't make a new session_id" unless $session_id;
print $q->redirect($q->script_name() . "/$session_id");
exit(0);
}
#--------------------------------(needed above)--------------
sub expire_old_sessions {
$DBH->do(<<END);
DELETE FROM $DB_TABLE
WHERE (unix_timestamp() - unix_timestamp(modified)) > $EXPIRE
END
}
#--------------------------------(also needed above)---------
sub generate_id {
my $tries = 0;
my $id = &hash($SECRET . rand());
while ($tries++ < $MAX_TRIES) {
last if
$DBH->do("INSERT INTO $DB_TABLE (session_id, acc) VALUES ('$id', 0)");
$id = &hash($SECRET . rand());
}
return undef if $tries >= $MAX_TRIES;
return $id;
}
sub hash {
my $value = shift;
return substr(MD5->hexhash($value), 0, $ID_LENGTH);
}
#--------------------------------(last one needed)-----------
sub check_id {
my $id = shift;
return $id
if $DBH->do("SELECT 1 FROM $DB_TABLE WHERE session_id = '$id'") > 0;
return $id
if $DBH->do("INSERT INTO $DB_TABLE (session_id, acc) VALUES ('$id', 0)");
return '';
}
#--------------------------------(retrieve acc)--------------
sub get_state {
my $id = shift;
my $query = "SELECT * FROM $DB_TABLE WHERE session_id = '$id'";
my $sth = $DBH->prepare($query) || die "Prepare: ", $DBH->errstr;
$sth->execute || die "Execute: ", $sth->errstr;
my $state = $sth->fetchrow_hashref;
$sth->finish;
return $state->{acc};
}
#--------------------------------(calculate new acc)---------
sub calculate {
my ($acc, $fun, $arg) = @_;
return $acc + $arg if $fun eq 'add';
return $acc - $arg if $fun eq 'sub';
return $acc;
}
#--------------------------------(store new acc)-------------
sub save_state {
my ($state, $id) = @_;
my $sth = $DBH->prepare(<<END) || die "Prepare: ", $DBH->errstr;
UPDATE $DB_TABLE
SET acc = ?
WHERE session_id = '$id'
END
$sth->execute($acc) || die "Execute: ", $DBH->errstr;
$sth->finish;
}
#--------------------------------(print current acc)---------
sub status {
my ($acc) = @_;
# $acc += 0;
print "The accumulator is currently $acc. <p>";
}
One can access it here. I hope you find this one easier to follow than the original Hangman game.
A348/A548 LAB ASSIGNMENT SIX
Two tasks:
<? phpinfo() ?>
script installed and working.
Two more tasks: