Sunday, May 1, 2011

Discussion Forums:Chapter 18


Chapter 18

Discussion Forums





CONTENTS


The World Wide Web discussion forum is a new way to deliver information the way Usenet newsgroups and computer bulletin board services have for years. The hyperlinking nature of the Web has made this migration quite natural for users and relatively simple to program through the Common Gateway Interface.

Discussion Forums-Everything Old Is New Again

Prior to the explosion of the World Wide Web, the User Network (Usenet) newsgroups were the most popular service on the Internet. They allowed users from all over the world to carry on a sort of "public discussion." The protocol used to transfer this information took heavily from the e-mail protocol, and local systems were obliged to, in essence, keep a local copy of Usenet for its users or face severe performance penalties. Usenet is still very popular today. In Usenet, you can find discussion groups on thousands of subjects.
Something very similar to Usenet can be simulated on the World Wide Web through CGI programming: a discussion forum. In fact, the earliest versions of Netscape allowed users to read Usenet newsgroups by parsing news articles and displaying them as HTML within the main Netscape client window. In this chapter, I'm going to accomplish the following:
  • Discuss the bistate display of a discussion forum
  • Plan data fields
  • Explore the concept of a threaded discussion forum
  • Develop a discussion forum
  • Explore discussion forum administration, including some accompanying source code
  • Offer suggestions as to extra features a discussion forum may have

Discussion Forum Display and Bistate CGI Programming

Though my opinions on a lot of things can run counter to the norm, when it comes to the display of a discussion forum, there tends to be a general degree of agreement between other programmers and me. Discussion forums tend to have two sorts of displays: an entry-level display and a display for people who are actually reading the postings within. This concept is demonstrated in Figure 18.1.
Figure 18.1: An outline of how discussion forums will look for users both entering into the forum and read.
Though the ends are generally agreed upon, the means can differ. Entries within a discussion forum can either be full HTML documents that are updated as needed, or they can be stored as files of information possessing header fields and a body area that are processed as needed by the CGI program and displayed from within the CGI. There are good arguments for both methods: creating "ready-made" HTML documents requires no CGI involvement at view-time, so pages can be displayed faster when being viewed. Dynamic presentation of postings can give a discussion forum much greater information-handling capabilities should they be required. For instance, if you wanted to create an auxiliary CGI program or routine that would display only messages by a given author, it would be much more straightforward to generate this sort of report if your data was handled as data files rather than as full HTML documents.
I often talk about the capability of CGI programs to be "multistate"; that is, they can present different faces to the user depending on circumstances. Circumstances include GET/POST information, environment variables (the IP the user is accessing the CGI program from, what browser he or she is using, what link the user clicked on to get to the CGI, and so on), the time of day, the HTTPd system load, phase of the moon, or just about anything else that can vary. I find that discussion forums naturally lend themselves to a very special multistate configuration-a bistate.
The division between just entering into the discussion forum and reading through it is an obvious one. In the first case, only two general areas are needed: a list of postings and a form for entering in your own posting.
If we are currently reading a posting, the discussion forum should display three areas: the contents of the posting being read, a list of related postings, and a form to submit a new posting. Any submission from the form in this area will relate the submission to the current posting. Also, this form can include "prequoted" text from the current posting to help the poster refer to specific elements in the message being replied to.

Useful Data Fields for Discussion Forums and Parent/Sibling/Child Relationships

Any posting in a discussion forum obviously includes a message body. Beyond that we get to choose what data fields we think are important to have included in a message. Here are the data fields that have gained general acceptance:
  • A subject line.
  • A To: field.
  • The name or handle of the author.
  • The author's e-mail address.
  • The time and date of posting. This is automatically generated by the discussion forum CGI.
  • The ID number of the posting to which the current posting is a reply.
  • The ID numbers of all postings that reply to this posting.
The first five fields in this list strongly resemble what you'd find in an e-mail. Part of this stems from the historical organization of discussion lists, but mostly these five fields are here because this sort of organization scheme just makes sense. The remaining two fields allow the CGI system to "thread" discussions.
Threading turns what might otherwise be white noise babble into meaningful conversations. After reading a message, the reader is given the opportunity to reply. The two messages are linked to each other with the reply as a "child message" and the first message as the "parent." A parent can have many children, which are "siblings." The discussion forum is responsible for organizing its postings in a way that reflects these familial relationships. On the Web, discussion forums often take advantage of the ordered and unordered list features of HTML. Figure 18.2 is a screen shot of the London Chat BBS, a discussion forum I have attached to one of my chat rooms. It illustrates the use of HTML list features to "thread" postings.
Figure 18.2: The London Chat BBS shows how discussion forums are well matched to Chat Rooms.

Note
The parent/sibling/child scheme is a way of conceptualizing the "tree" data structure. If ever you want to design a particularly complicated discussion forum, it might be useful to consult a reference on trees. The Art of Computer Programming by Donald Knuth is the classic book in the field of data structures, but it likely would be overkill for your problem. Any good book store or library should be able to cough up a book that will work out for you. I use Data Structures and Program Design in C by Kruse, Leung, and Tondo.


A Discussion Forum Example

Listing 18.1 is the source code for my example discussion forum. It works along the model of CGI processing of data files rather than generation of complete HTML documents. You can see this discussion forum on-line at
http://www.anadas.com/cgiunleashed/discussion/forum.cgi

Listing 18.1. forum.cgi-A "threaded discussion forum" CGI program, written in Perl.
#!/usr/bin/perl

#
# This program was written by Richard Dice of Anadas Software Development
# as part of the Sams Net "CGI Programming Unleashed" book.  The author
# intends this code to be used for instructional purposes and not for
# resale or commercial gain.
#
# Any questions or comments regarding this program are welcome.  You
# may contact the author by Internet email: rdice@anadas.com
#

# --- Configuration Variables ---
$prog_url = 'http://www.anadas.com/cgiunleashed/discussion/forum.cgi';

#
# the Query String is treated as a prefix to qualify which forum postings
# are ones of interest. 
#
$prefix = $ENV{QUERY_STRING};

# --- start of GET/POST method handler ---

# puts all POST query information the variable $input_line
read(STDIN, $input_line, $ENV{CONTENT_LENGTH});

# replace all '+' coded spaces with real spaces
$input_line =~ tr/+/ /;

# creates array of all data files in $input_line from & separated info
@fields = split(/\&/,$input_line);
undef($input_line); # free up memory

#
# decodes hex info for each name/value pair and places pairs in
# %input associative array
#
foreach $i (0 .. $#fields) {
   ($name,$value) = split(/=/,$fields[$i]);
   $name =~ s/%(..)/pack("c",hex($1))/ge;
   $value =~ s/%(..)/pack("c",hex($1))/ge;
   $input{$name} = $value;
}

# --- end of GET/POST method handler ---

#
# should this page be accessed with form data and having a refering URL
# different from $prog_url, someone is attempting to post data from
# an invalid form -- exit the program with error message
#
if ( defined(%input) || ($ENV{HTTP_REFERER} ne $prog_url) ) {
   &referer_error;
}

if ( $input{'submit'} eq 'Submit this Article' ) {
   &new_article;
   print "Location: $prog_url?$input{'basepost'}\n\n";
   exit 0;
}

#
# gets the names of all relevent posting files and puts in the @posts array
#
&get_posts($prefix);

if ( $prefix eq '' ) {
   &print_header;
   &print_list;
   &print_form;
   &print_footer;
} else {

   &read_article($prefix);

   &print_header;
   &print_posting;
   &print_list;
   &print_form;
   &print_footer;
}

exit 0;

sub print_header {

   print "Content-type: text/html\n\n";
   if ( $prefix eq '' ) {
      print <<END;
<HTML><HEAD><TITLE>Discussion Forum</TITLE></HEAD>
<BODY>
<P>
<H3>All Discussion Forum Postings</H3>
END
   } else {
      print "<HTML><HEAD><TITLE>Posting: $title</TITLE></HEAD>\n";
      print "<BODY>\n";
      print "<P>\n";
      print "<A HREF=$prog_url>Return to Discussion Forum front page</A>\n";
   }
}

sub referer_error {

   print <<END;
<HTML><HEAD><TITLE>Refering URL Error!</TITLE></HEAD>
<BODY>
<P>
The form which was submitted to this CGI program did not originate with
this program.  This is forbidden.
<P>
<A HREF=$prog_url>Return to the Discussion Forum</A>
</BODY></HTML>
END

}

sub no_author_error {

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>No Author Error!</TITLE></HEAD>
<BODY>
<FONT SIZE=+1><B>
<P>
No author name was entered into the posting form.  One is required.<BR>
Press the BACK button on your browser to re-enter the form.</B></FONT>
</BODY></HTML>
END
   exit 1;
}

sub no_subject_error {

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>No Subject Error!</TITLE></HEAD>
<BODY>
<FONT SIZE=+1><B>
<P>
No subject was entered into the posting form.  One is required.<BR>
Press the BACK button on your browser to re-enter the form.</B></FONT>
</BODY></HTML>
END
   exit 1;
}

sub print_list {

   if ( $prefix ne '' ) {
      print "<H3>Follow-up Postings:</H3>\n";
      shift(@posts);
   }

   if ( $#posts == -1 ) {
      print "<H4>None</H4>\n<HR>\n";
      return 0;
   }

#
# reconstructs header fields from posting and place into the list
#
   $ul_count = 0;
   foreach $i ( 0 .. $#posts ) {

#
# this if structure implements my "typographical trick" which threads
# postings based solely on their file names.  "bits" of posting names,
# separated by '_', are compared, first to see how many left-most bits
# the current posting has in common with its immediate predecessor,
# and then the signed inequality of right-most not-in-common bits is
# used to determine the extent of <UL> pushing/popping needed to thread
#
      if ( $i != 0 ) {

         $this = $posts[$i];
         $previous = $posts[$i-1];

         @this_bits = split(/-/,$this);
         @previous_bits = split(/-/,$previous);

         if ( $#this_bits > $#previous_bits ) {
            $lesser_bits = $#previous_bits;
         } else {
            $lesser_bits = $#this_bits;
         }

         $common = 0;
         for $j ( 0 .. $lesser_bits ) {
            last if $this_bits[$j] ne $previous_bits[$j];
            $common++;
         }
   
         splice(@this_bits,$[,$common);
         splice(@previous_bits,$[,$common);
   
         if ( $common == 0 ) {
            while ( $ul_count ) {
               print "</UL>";
               $ul_count-;
            }
         } else {
            if ( $#this_bits > $#previous_bits ) {
               for $k ( 1 .. ($#this_bits - $#previous_bits) ) {
                  print "<UL>\n";
                  $ul_count++;
               }
            } elsif ( $#this_bits < $#previous_bits ) {
               for $k ( 1 .. ($#previous_bits - $#this_bits) ) {
                  print "</UL>\n";
                  $ul_count-;
               }
            }
         }
      } else {
         $this = $posts[0];
         print "<UL>\n";
      }

      open(POST,"$this.post");
      &read_header;
      close(POST);

      print "<LI> <A HREF=$prog_url?$this>$subject</A> ";
      if ( $email ne '' ) {
         print "<FONT SIZE=-1><A HREF=mailto:$email><B>$author</B></A> ";
      } else {
         print "<FONT SIZE=-1><B>$author</B> ";
      }
      if ( $to ne '' ) { print "<I>To:</I> <B>$to</B> "; }
      print "<I>$time</I></FONT>\n";
   }

   while ( $ul_count ) {
      print "</UL>";
      $ul_count--;
   }
   print "</UL>\n<HR>\n";
}

sub get_posts {

   local($pre);
   ($pre) = @_;

   @posts = 'ls -r1 $pre*.post';
   $post_len = length(".post\n");
   foreach $post ( @posts ) {
      substr($post,-$post_len) = ''; # remove unwanted tailing characters
   }
}

sub new_article {

   $temp = $input{'author'};
   $temp =~ s/\s//;
   if ( $temp eq '' ) {
      &no_author_error;
   }
   $temp = $input{'subject'};
   $temp =~ s/\s//;
   if ( $temp eq '' ) {
      &no_subject_error;
   }

   &get_posts($input{'basepost'});

   if ( $#posts == -1 ) {
#
# case where there are absolutely no postings in the tree
#
      $fname = '00000';
   } else {
      if ( $input{'basepost'} eq '' ) {
#
# case where we are adding a new posting to the base level of the tree
#
         @parts = split(/-/,$posts[0]);
         $fname = ++($parts[0]);
      } else {
         $temp = shift(@posts);
         if ( $#posts == -1 ) {
#
# case of a first reply
#
            $fname = $temp . '-' . '00000';
         } else {
#
# case of each subsequent reply
#
            @parts = split(/-/,$posts[0]);
            ++($parts[$#parts]);
            $fname = join('-',@parts);
         }
      }
   }

#
# gets the current system time and removes some info for the sake of
# horizontal space (some info = seconds & time zone)
#
   chop($time = 'date');
   @timefields = split(/[\s]+/,$time);
   substr($timefields[3],-3,3) = '';
   $time = join(' ',@timefields[0..3],$timefields[5]);

#
# remove DOS-style newline info from posting body
#
   $input{'body'} =~ s/\cM//g;

   open(POST,"> $fname.post");
   print POST "Author\t$input{'author'}\n";
   print POST "Email\t$input{'email'}\n";
   print POST "Subject\t$input{'subject'}\n";
   print POST "To\t$input{'to'}\n";
   print POST "Time\t$time\n";
   print POST $input{'body'};
   close(POST);

   chmod 0660,"$fname.post";
}

sub footer {
   print "</BODY></HTML>\n\n";
}

sub print_form {

   if ( $prefix eq '' ) {
      print <<END;
<H3>Submit a New Posting</H3>
<FORM METHOD=POST ACTION=$prog_url>
<INPUT TYPE=HIDDEN NAME="basepost" VALUE="$prefix">
<PRE>
Author  : <INPUT TYPE=text NAME="author">
Email   : <INPUT TYPE=text NAME="email"> (optional)
Subject : <INPUT TYPE=text NAME="subject">
To      : <INPUT TYPE=text NAME="to"> (optional) <BR>
Body of Article:
<TEXTAREA NAME="body" ROWS=5 COLS=50></TEXTAREA></PRE>
<INPUT TYPE=SUBMIT NAME="submit" VALUE="Submit this Article">
<INPUT TYPE=RESET NAME=clear Value="Clear this form">
</FORM>
END
   } else {

      &read_article($prefix);

      print <<END;
<H3>Reply to this Posting</H3>
<FORM METHOD=POST ACTION=$prog_url>
<INPUT TYPE=HIDDEN NAME="basepost" VALUE="$prefix">
<PRE>
Author  : <INPUT TYPE=text NAME="author">
Email   : <INPUT TYPE=text NAME="email"> (optional)
Subject : <INPUT TYPE=text NAME="subject" VALUE="Re: $subject">
To      : <INPUT TYPE=text NAME="to"  VALUE=\"$author\"> (optional)
<BR> Body of Article:
END
      print "<TEXTAREA NAME=\"body\" ROWS=5 COLS=50>";
      foreach ( @body ) { print ":: $_"; }
         print "</TEXTAREA></PRE>\n" ,
"<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"Submit this Article\">\n" ,
"<INPUT TYPE=RESET NAME=clear Value=\"Clear this form\">\n" , "</FORM>\n";
   }
}

sub read_article {

   local ($post_id);

   ($post_id) = @_;

   open(POST,"$post_id.post");
   &read_header;
   @body = <POST>;
   close(POST);

}

sub read_header {

   chop($author = <POST>);
   ($discard,$author) = split(/\t/,$author);
   chop($email = <POST>);
   ($discard,$email) = split(/\t/,$email);
   chop($subject = <POST>);
   ($discard,$subject) = split(/\t/,$subject);
   chop($to = <POST>);
   ($discard,$to) = split(/\t/,$to);
   chop($time = <POST>);
   ($discard,$time) = split(/\t/,$time);

}

sub print_posting {

   print "<HR>\n";

   &read_article($prefix);

   if ( $email ne '' ) {
      print "<A HREF=mailto:$email>$author</A> ";
   } else {
      print "$author ";
   }
   print "on <I>$time</I> said:\n";
   print "<H2>$subject</H2>\n";

   foreach $line ( @body ) {
      $line =~ s/\n/<BR>\n/g;
      print $line;
   }
   print "<BR><HR>\n";

}

sub print_footer {

   print "</BODY></HTML>\n";

}

My general philosophy when programming is to find the sneakiest way of doing something. I try to use the tools of a language to do something that likely no one ever thought of doing before. I do this to avoid real work.
There are a great number of studied and well understood data structures in computer science that people use to get jobs done. When you need a solution, you go to your books or your source code libraries and invoke them as prescribed. Unless absolutely necessary, I'm too much of a loner to plug in someone else's code, and I'm too lazy to recode a classical solution from the ground up. Instead, I come up with a hack.
My hack for this program is to name data files so that the postings are automatically threaded for me, more or less. A top-level posting (one with no parent) will be named XXXXX-post, where the Xs are digits. The program assigns 00000 to the first post, and each subsequent first-level post is given the value of the most recent post plus 1. A reply to this will be XXXXX.XXXXX-post. A reply to this would be XXXXX.XXXXX.XXXXX-post, and so on. I get these in the correct order by the Perl command
@posts = 'ls -1r *-post';
The automatic ASCII collating of the ls command will arrange the postings in order of oldest to newest, given the numbering scheme I described in the preceding paragraph. The -1r flag reverses the ordering so that the newest postings will be on the top of the list. The program keeps a count of how many dots (.) there are within a file's name, and this acts as the basis for determining which level of the "family tree" a posting rests on.
The bistate nature of this discussion forum is controlled by the QUERY_STRING environment variable. Though all posting-oriented forum information is passed to the program through the form using the POST method, I'm "manually" post-pending a query string to the URL that invokes the chat room. If this string is empty, the CGI program knows that it is being called directly and that it should show its "entry-level" face. If there is a string there, it displays the posting that corresponds to that string and the family of postings that surround that posting. This string is put to use quite simply. Called $prefix, the place where it is most important is
@posts = 'ls -1r $prefix*-post';
This is the form of the line that is actually in use within the program. With this simple trick, the CGI program will focus in on only those posts that are relevant to the user's needs.

Discussion Forum Administration

In computer programming, even common sense questions have to be answered explicitly: once postings are entered into the discussion forum, how do they get removed? Do they ever get removed?
The answer to the second question is yes-of course they get removed. A discussion forum with several hundred thousand messages in it is just about as worthless as one with no messages at all. The problem then becomes finding a good way to accomplish these posting removals. I'll outline a few popular options.

Remove Posting by Date

This is a very popular option because it allows a very natural organization: old postings go, and new ones stay. There are three ways to program this feature: manually controlled by the discussion forum administrator, automatically by the discussion forum, or through an auxiliary program as a cron job.
The cron command is a UNIX daemon that runs other commands according to time-based rules found within the various crontab files. These rules tell cron which command to run, how, and when. If the cron method is used to clean out old postings within the discussion forum, the solution isn't strictly CGI. The discussion forum programmer would be responsible for writing a crontab file that details how cron should run another program, written to clean out the discussion forum.
If the job of cleaning out old postings is given to the discussion forum CGI itself, the strategy would be that whenever the discussion forum was activated, it would scan through its postings and remove ones that fit some programmed criteria of age.

Remove Thread by Date

This option for removing articles in a discussion forum will look for the first (and therefore oldest) file in a thread and will delete all postings in that thread. The mechanisms that invoke this option are identical to those for removing individual postings by date.

Remove Posting by Author

This is a handy feature to have in the event that a discussion forum is graced by a less than graceful individual. Programming this sort of removal is highly dependent upon the programming language being used and the overall organization of the discussion forum.

Remove Individual Postings

This option can be the easiest to program but requires the most effort on the part of the discussion forum administrator. The only consideration with this option is how the program will deal with threads left tattered by the procedure. In discussion forum setups based on data files and dynamic generation of lists, this problem wouldn't normally be too great. With discussion forums that deal with complete HTML files, the easy part is removing the data file. The hard part is hunting through all the other HTML files for references to that file and killing those references. Re-threading the discussion forum might be appropriate or needed.

Remove Individual Threads

This option is the same as the preceding but focuses on whole threads rather than individual postings.
The following code, Listing 18.2, will remove postings from forum.cgi by threads, authors, dates, and individual postings. I have not included any automatic date removal features.

Listing 18.2. admin.cgi-The Discussion Forum administration program that accompanies forum.cgi.
#!/usr/bin/perl

#
# This program was written by Richard Dice of Anadas Software Development
# as part of the Sams Net "CGI Programming Unleashed" book.  The author
# intends this code to be used for instructional purposes and not for
# resale or commercial gain.
#
# Any questions or comments regarding this program are welcome.  You
# may contact the author by Internet email: rdice@anadas.com
#

#
# the timelocal.pl library is needed for access to the &timegm() subroutine,
# which I use in the sorting of dates
#
require "timelocal.pl";

# -- Configuration Variables --
$prog_url = 'http://www.anadas.com/cgiunleashed/discussion/admin.cgi';
$forum_url = 'http://www.anadas.com/cgiunleashed/discussion/forum.cgi';

# -- start of GET/POST method handler --

# puts all POST query information the variable $input_line
read(STDIN, $input_line, $ENV{CONTENT_LENGTH});

# replace all '+' coded spaces with real spaces
$input_line =~ tr/+/ /;

# creates array of all data files in $input_line from & separated info
@fields = split(/\&/,$input_line);
undef($input_line); # free up memory

#
# decodes hex info for each name/value pair and places pairs in
# %input associative array
#
foreach $i (0 .. $#fields) {
   ($name,$value) = split(/=/,$fields[$i]);
   $name =~ s/%(..)/pack("c",hex($1))/ge;
   $value =~ s/%(..)/pack("c",hex($1))/ge;
   $input{$name} = $value;
}

# --- end of GET/POST method handler ---

#
# should this page be accessed with form data and having a refering URL
# different from $prog_url, someone is attempting to post data from
# an invalid form -- exit the program with error message
#
if ( defined(%input) || ($ENV{HTTP_REFERER} ne $prog_url) ) {
   &referer_error;
}

#
# Program will switch on the query string.  Also, the program is being
# invoked via a form submission, then actual deleting of postings needs
# to be done and not just displaying the option which queries for which
# postings to delete
#
$mode = $ENV{QUERY_STRING};
if ( !defined($input{'method'}) ) {
   &query_remove if ( ($mode eq 'posts') || ($mode eq 'thread') );
   &query_date_remove if $mode eq 'date';
   &query_author_remove if $mode eq 'author';
} else {
   &remove_posts if $input{'method'} eq 'posts';
   &date_remove if $input{'method'} eq 'date';
   &thread_remove if $input{'method'} eq 'thread';
   &author_remove if $input{'method'} eq 'author';
}

#
# if no query string nor $input{'method'} is found in the invoking of this
# page, present an intro page which supplies a menu of options
#
&intro_page;

exit 0;

#
# display this page if an invalid form submission is being made
#
sub referer_error {

   print <<END;
<HTML><HEAD><TITLE>Refering URL Error!</TITLE></HEAD>
<BODY>
<P>
The form which was submitted to this CGI program did not originate with
this program.  This is forbidden.
<P>
<A HREF=$prog_url>Return to the Discussion Forum</A>
</BODY></HTML>
END

}

#
# the following subroutine presents a threaded list of postings which
# may be removed by either thread or individual posting, depending on
# the value of the $mode variable
#
sub query_remove {

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>Discussion Forum Administration</TITLE></HEAD>
<BODY>
END
   &get_posts;

   if ( $#posts == -1 ) {
      print "<H4>No postings found</H4>\n</BODY></HTML>";
      return 0;
   } elsif ( $mode eq 'posts' ) {
      print "<H3>Discussion Forum Administration : Remove Individual",
      " Postings</H3>\n";
      print "<FORM METHOD=POST ACTION=$prog_url>\n";
      print "<INPUT TYPE=HIDDEN NAME=method VALUE=\"posts\">\n";

      $code = 'POST';
   } elsif ( $mode eq 'thread' ) {
      print "<H3>Discussion Forum Administration : Remove Threads</H3>\n";
      print "<P>Clicking in a checkbox will remove that posting and all",
      "its children upon form submission.</P>\n";
      print "<FORM METHOD=POST ACTION=$prog_url>\n";
      print "<INPUT TYPE=HIDDEN NAME=method VALUE=\"thread\">\n";

      $code = 'THREAD';
   }

#
# reconstructs header fields from posting and place into the list
#
   $ul_count = 0;
   foreach $i ( 0 .. $#posts ) {

#
# this if structure implements my "typographical trick" which threads
# postings based solely on their file names.  "bits" of posting names,
# separated by '_', are compared, first to see how many left-most bits
# the current posting has in common with its immediate predecessor,
# and then the signed inequality of right-most not-in-common bits is
# used to determine the extent of <UL> pushing/popping needed to thread
#
      if ( $i != 0 ) {

         $this = $posts[$i];
         $previous = $posts[$i-1];

         @this_bits = split(/-/,$this);
         @previous_bits = split(/-/,$previous);

         if ( $#this_bits > $#previous_bits ) {
            $lesser_bits = $#previous_bits;
         } else {
            $lesser_bits = $#this_bits;
         }

         $common = 0;
         for $j ( 0 .. $lesser_bits ) {
            last if $this_bits[$j] ne $previous_bits[$j];
            $common++;
         }
   
         splice(@this_bits,$[,$common);
         splice(@previous_bits,$[,$common);
   
         if ( $common == 0 ) {
            while ( $ul_count ) {
               print "</UL>";
               $ul_count--;
            }
         } else {
            if ( $#this_bits > $#previous_bits ) {
               for $k ( 1 .. ($#this_bits - $#previous_bits) ) {
                  print "<UL>\n";
                  $ul_count++;
               }
            } elsif ( $#this_bits < $#previous_bits ) {
               for $k ( 1 .. ($#previous_bits - $#this_bits) ) {
                  print "</UL>\n";
                  $ul_count--;
               }
            }
         }
      } else {
         $this = $posts[0];
         print "<UL>\n";
      }

      open(POST,"$this.post");
      &read_header;
      close(POST);

      print "<LI><I>remove</I> <INPUT TYPE=checkbox NAME=\"$code$this\"> ";
      print "<FONT SIZE=-1><A HREF=$forum_url?$this>$subject</A> ";
      print "$author ";
      if ( defined($to) ) { print "<B><I>To:</I></B> $to "; }
      print "<I><B>On:</B> $time</I></FONT>\n";
   }

   while ( $ul_count ) {
      print "</UL>";
      $ul_count--;
   }
   print "</UL>\n<HR>\n";

   print "<P><INPUT TYPE=SUBMIT NAME=submit VALUE=\"Submit this form\">\n";
   print "<INPUT TYPE=reset NAME=reset VALUE=\"Reset this form\">\n";
   print "\n</FORM>\n";

   print "</BODY></HTML>\n";
   exit 0;
}

#
# - it's easy to remove posts -- just delete the corresponding .post file
# - proper threading is maintained by the spiffy "typographic data structure"
#
sub remove_posts {

   foreach $key ( keys %input ) {
      if ( $key =~ /POST/ ) {
         substr($key,$[,4) = '';
         system("rm $key.post");
      }
   }
   &report_removal;
}

#
# - it's easy to remove threads -- just delete the corresponding .post file
# - proper threading is maintained by the spiffy "typographic data structure"
#
sub thread_remove {

   foreach $key ( keys %input ) {
      if ( $key =~ /THREAD/ ) {
      substr($key,$[,6) = '';
      system("rm $key*.post");
      }
   }
   &report_removal;
}

#
# generate the form which asks for names of to-be-removed authors
#
sub query_author_remove {

   &get_posts;

#
# build list of author names and their corresponding postings by
# parsing the entire collection of postings
#
   foreach $post ( @posts ) {
      open(POST,"$post.post");
      &read_header;
      $allauthors{$author} .= "<LI><A HREF=$forum_url?$post>$subject</A> ";
      $allauthors{$author} .= "$author ";
      if ( defined($to) ) { $allauthors{$author} .= "<B><I>To:</I></B> $to "; }
      $allauthors{$author} .= "<I><B>On:</B> $time</I>\n";
      close(POST);
   }
   foreach $key ( keys(%allauthors)) {
      substr($allauthors{$key},$[,0) = "<UL>\n";
      substr($allauthors{$key},-1,0) = "\n</UL>\n";
   }

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>Discussion Forum Administration</TITLE></HEAD>
<BODY>
<H3>Discussion Forum Administration : Remove Postings by Author</H3>
<FORM ACTION=$prog_url METHOD=POST>
<INPUT TYPE=HIDDEN NAME=method VALUE="author">
<TABLE BORDER CELLPADDING=8>
<TR><TH VALIGN=TOP ALIGN=CENTER>Author Name</TH>
<TH VALIGN=TOP ALIGN=CENTER>Check to Remove</TH>
<TH VALIGN=TOP ALIGN=CENTER>This Author's Postings</TH></TR>
END
   foreach $authname (sort case_insensitive (keys(%allauthors))) {
      print "<TR><TD VALIGN=TOP ALIGN=LEFT>$authname</TD>\n";
      print "<TD VALIGN=TOP ALIGN=CENTER><INPUT TYPE=chECKBOX NAME=\"AUTHOR" .
      &hex_encode($authname) . "\"></TD>\n";
      print "<TD VALIGN=TOP ALIGN=LEFT>$allauthors{$authname}</TD></TR>\n";
   }

   print <<END;
</TABLE>
<P><INPUT TYPE=SUBMIT NAME=submit VALUE="Submit this form">
<INPUT TYPE=reset NAME=reset VALUE="Reset this form"></FORM>
</BODY></HTML>
END
   exit 0;
}

#
# it's easy to remove postings by authors -- once you have the author's
# name, just scan through all header fields and check to see if the name
# in the header field matches the name to be deleted
#
sub author_remove {

   local($i,$mark);

   foreach $auth ( keys(%input) ) {
      next if !($auth =~ /^AUTHOR/);
      substr($auth,$[,length('AUTHOR')) = '';
      $ex_authors[$i++] = &hex_decode($auth);
   }

   &get_posts;

POSTINGS:   foreach $post ( @posts ) {
      open(POST,"$post.post");
      &read_header;
      close(POST);
      foreach $entry ( @ex_authors ) {
         if ( $author eq $entry ) {
            system("rm $post.post");
            next POSTINGS;
         }
      }
   }
   &report_removal;
}

#
# generate the form which the user fills in to decide which dates are
# supposed to be removed from the body of discussion forum postings
#
sub query_date_remove {

   &get_posts;

   foreach $post ( @posts ) {
      open(POST,"$post.post");
      &read_header;
      @tfields = split(/[\s]+/,$time);
      $timetemp = join(' ',@tfields[0..2],$tfields[4]);
      $dates{$timetemp} .= "<LI><A HREF=$forum_url?$post>$subject</A> ";
      $dates{$timetemp} .= "$author ";
      if ( defined($to) ) { $dates{$timetemp} .= "<B><I>To:</I></B> $to "; }
      $dates{$timetemp} .= "<I><B>On:</B> $time</I>\n";
      close(POST);
   }
   foreach $key ( keys(%dates)) {
      substr($dates{$key},$[,0) = "<UL>\n";
      substr($dates{$key},-1,0) = "\n</UL>\n";
   }

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>Discussion Forum Administration</TITLE></HEAD>
<BODY>
<H3>Discussion Forum Administration : Remove Postings by Date</H3>
<FORM ACTION=$prog_url METHOD=POST>
<INPUT TYPE=HIDDEN NAME=method VALUE="date">
<TABLE BORDER CELLPADDING=8>
<TR><TH VALIGN=TOP ALIGN=CENTER>Date</TH>
<TH VALIGN=TOP ALIGN=CENTER>Check to Remove</TH>
<TH VALIGN=TOP ALIGN=CENTER>Postings on this Date</TH></TR>
END
   foreach $date (sort by_date (keys(%dates)) ) {
      print "<TR><TD VALIGN=TOP ALIGN=LEFT>$date</TD>\n";
      print "<TD VALIGN=TOP ALIGN=CENTER><INPUT TYPE=chECKBOX NAME=\"DATE" .
      &hex_encode($date) . "\"></TD>\n";
      print "<TD VALIGN=TOP ALIGN=LEFT>$dates{$date}</TD></TR>\n";
   }

   print <<END;
</TABLE>
<P><INPUT TYPE=SUBMIT NAME=submit VALUE="Submit this form">
<INPUT TYPE=reset NAME=reset VALUE="Reset this form"></FORM>
</BODY></HTML>
END
   exit 0;
}

#
# actually does the grunt-work of removing the
# previously-selected "bad dates"
#
sub date_remove {

   local($i,$mark);

   foreach $date ( keys(%input) ) {
      next if !($date =~ /^DATE/);
      substr($date,$[,length('DATE')) = '';
      $bad_dates[$i++] = &hex_decode($date);
   }

   &get_posts;

POSTINGS:   foreach $post ( @posts ) {
      open(POST,"$post.post");
      &read_header;
      close(POST);
      @tfields = split(/[\s]+/,$time);
      $time = join(' ',@tfields[0..2],$tfields[4]);
      foreach $date ( @bad_dates ) {
         if ( $time eq $date ) {
            system("rm $post.post");
            next POSTINGS;
         }
      }
   }
   &report_removal;
}

sub report_removal {

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>Discussion Forum Administration</TITLE></HEAD>
<BODY>
<H3>Postings Successfully Removed</H3>
<P>
<A HREF=$prog_url>Return to the Discussion Forum Administration Page</A>
</BODY></HTML>
END

   exit 0;
}

sub get_posts {

   @posts = 'ls -r1 *.post';
   $post_len = length(".post\n");
   foreach $post ( @posts ) {
      substr($post,-$post_len) = ''; # remove unwanted tailing characters
   }
}

sub read_article {

   local ($post_id);

   ($post_id) = @_;

   open(POST,"$post_id.post");
   &read_header;
   @body = <POST>;
   close(POST);
}

sub read_header {

   chop($author = <POST>);
   ($discard,$author) = split(/\t/,$author);
   chop($email = <POST>);
   ($discard,$email) = split(/\t/,$email);
   chop($subject = <POST>);
   ($discard,$subject) = split(/\t/,$subject);
   chop($to = <POST>);
   ($discard,$to) = split(/\t/,$to);
   chop($time = <POST>);
   ($discard,$time) = split(/\t/,$time);
}

sub intro_page {

   print "Content-type: text/html\n\n";
   print <<END;
<HTML><HEAD><TITLE>Discussion Forum Administration</TITLE></HEAD>
<BODY>
<H3>Discussion Forum Administration</H3>
<P>
Please choose one of the following methods for removing postings from the
discussion forum:
<UL>
<LI><A HREF=$prog_url?posts>Remove Individual Postings</A>
<LI><A HREF=$prog_url?date>Remove Postings by Date</A>
<LI><A HREF=$prog_url?thread>Remove Postings by Thread</A>
<LI><A HREF=$prog_url?author>Remove Postings by Author</A>
</UL>
</BODY></HTML>
END

}

#
# this function is used by the sort command when a case-insensitive string
# comparison is performed... for instance, in a situation where I want 'a'
# to come before 'Z' rather than after, as would usually be the case
# given that Z comes before a in the ASCII sequence
#
sub case_insensitive {

   local($atemp,$btemp);
   $atemp = $a; $btemp = $b;

   $atemp =~ tr/A-Z/a-z/;
   $btemp =~ tr/A-Z/a-z/;

   $atemp cmp $btemp;
}

#
# this function is used by the sort command when trying to compare the
# dates of two postings
#
sub by_date {

   local($akey,$bkey);

   $akey = $a;
   $bkey = $b;

   @afields = split(/[\s]+/,$akey);
   @bfields = split(/[\s]+/,$bkey);

   substr($afields[3],$[,2) = '';
   substr($bfields[3],$[,2) = '';

   $months{'Jan'} = 0;
   $months{'Feb'} = 1;
   $months{'Mar'} = 2;
   $months{'Apr'} = 3;
   $months{'May'} = 4;
   $months{'Jun'} = 5;
   $months{'Jul'} = 6;
   $months{'Aug'} = 7;
   $months{'Sep'} = 8;
   $months{'Oct'} = 9;
   $months{'Nov'} = 10;
   $months{'Dec'} = 11;
   $weekday{'Sun'} = 0;
   $weekday{'Mon'} = 1;
   $weekday{'Tue'} = 2;
   $weekday{'Wed'} = 3;
   $weekday{'Thu'} = 4;
   $weekday{'Fri'} = 5;
   $weekday{'Sat'} = 6;

   &timegm('0','0','0',$afields[2],$months{$afields[1]},$afields[3],
   $weekday{$afields[0]},'','') <=>
   &timegm('0','0','0',$bfields[2],$months{$bfields[1]},$bfields[3],
   $weekday{$bfields[0]},'','');

}

#
# I hex-encode certain fields to avoid the possibility that info within
# the fields will botch up certain HTML situations.
#
sub hex_encode {

   local($an,$temp);
   ($an) = @_;

   undef($temp);
   for $i ( 0 .. (length($an)-1) ) {
      $temp .= sprintf("%lx",ord(substr($an,$[+$i,1)));
   }
   $temp;
}

#
# hex-decoding is necessary to retrieve info that was hex-encoded before
#
sub hex_decode {

   local($acode,$temp,$t);
   ($acode) = @_;

   undef($temp);
   while ( $acode ) {
      $t = substr($acode,$[,2);
      substr($acode,$[,2) = '';
      $temp .= pack("c",hex($t));
   }
   $temp;
}

Discussion Forum Additions

Now that the basic concept of a discussion forum and its administration has been established, let's consider what sorts of useful "bells and whistles" can be brought to the field.

Selective Sorting Criteria

Much as articles could be deleted according to author or subject in addition to date, coding in an option so that the list of discussion forum postings could be displayed in any of these ways could be a welcome addition.

Search Engines

Sometimes threads and subject lines won't be enough when it comes to finding something you want within a discussion forum. A search engine that would scan through all postings looking for certain words or phrases and returning with a report of relevant articles might be a great boon for a large discussion forum. This feature is found within many Usenet newsgroup readers, and many of the modern Web search engines also include an option to search a database of Usenet postings, as well.

Registered Users and .htaccess Schemes

If the discussion forum was meant to be a sort of "company intranet" solution, it would be mandatory to restrict access to only those people who were authorized. One means of accomplishing this is the .htaccess system. Figure 18.3 is a screen shot showing a .htaccess-inspired pop-up dialog box.
Figure 18.3: A view of the hataccess scheme, as interpreted by Netscape.
The .htaccess scheme is an intrinsic feature of most Web servers. To create a .htaccess security system, the Web programmer will need to place a .htaccess file in the directory that is to be protected. The .htaccess file references files that are usually called .htgroup and .htpasswd. The creation and use of these files would justify an entire chapter or two within this book, so I can't go into all the details here. One important point I will leave you with is this: The User Name typed into the appropriate field in the .htaccess pop-up dialog box will be returned as the REMOTE_USER environment variable. Given this tidbit of information, your discussion forum can have precise knowledge of who is making (and even reading!) postings.

Summary

In your CGI programming experiences, you'll often find an excuse to include a discussion forum. I find them to be one of the most marketable aspects of the field. A full appreciation of them will pay you dividends.
Conceptually, discussion forums are based on the Usenet newgroups. Understandably, they share many of the same data fields. As a discussion forum programmer, you must ask yourself if you want these fields to be embedded within pre-processed HTML documents or as part of a header section in postings that are meant to be parsed each and every time they are reviewed.
Related to this choice is how you organize your parent/child hierarchy. How will you build family trees at run time? How will you permanently store that information?
No discussion forum is complete without an accompanying administration utility. This will no doubt be intimately related to how the discussion forum operates and should include different ways of tending to the postings.
Before I end this chapter, I want to point out a popular CGI discussion forum system. This is the WWWBoard of Matt's Script Archive. This discussion forum uses the "ready-made HTML file" philosophy, in contrast to how I've done things in this chapter. You can find this CGI system at
http://worldwidemart.com/scripts/wwwboard.shtml
Matt's Script Archive is an impressive CGI resource, and I take my hat off to Matt for his work in promoting, creating, and distributing quality CGI programs.






Voting Booths:Chapter 17


Chapter 17

Voting Booths





CONTENTS


While there is no special technical distinction to gathering information in a "voting booth" style, voting booths are used quite often on the Web. A large part of this popularity is due to the social familiarity with surveys, polling, and voting. Also, the structure of a voting booth lends itself well to the technology of forms. The measure of a voting booth is more a function of the database back end than the complexity of the form that creates the data. This chapter presents examples of simple and complicated voting booth CGI programs.

Voting Booths-Gathering and Managing Opinions

Since the Web began, people have been using it to gather information. An interesting turn-around can be engineered with CGI programming: Web pages can use people to gather information. There are many different strategies on how to accomplish this: the soliciting of orders, the active tracking of users, and the creation of threaded discussion areas are a few examples you'll find in this book. In addition to these methods, a very popular scheme for gathering information from users is the voting booth.
The concept of a voting booth should be intuitively obvious, but at the same time the words "intuitively obvious" seem like the computer-industry platitude equivalent to "at least you still have your health." With this in mind, I'll take a second to describe the procedure of on-line voting:
  1. A user loads an HTML page with a form area.
  2. The user answers questions posed by the form.
  3. The user submits the form through either a SUBMIT button or an Imagebutton.
  4. The form data is sent to a CGI program for parsing and processing.
  5. Reports based on the submitted data are generated. These reports could be in the form of dynamically generated Web pages, e-mail, server-based data files, images created on-the-fly, and so on.
This sequence of actions should be very familiar to anyone who has a moderate degree of Web experience. The problem is that this sequence isn't unique; these steps could equally well describe a shopping cart or other sorts of order entry, too. On one level, there is no essential difference between a voting booth and these sorts of CGI applications. This is due to the structure imposed on users by forms and the client-server model of the Web. So long as forms control the ways HTML programmers can ask users for input, the GET/POST methods circumscribe the data encoding format, and permanent information must be stored and organized server-side, we aren't likely to see a revolution in how CGI programming is performed. Still, there is a special "feel" to surveys, voting booths, and polling stations that deserves in-depth analysis.

Starting Simple-A Low-Level Voting Booth

Though obvious, it must be stated that there are both good and bad voting booths on the Web. I think the difference between the two is
  • The quality of the report generated by the CGI program
  • The degree of data management capability the voter and administrator have over the votes
To start this discussion, I've created an example of a reasonably simple voting booth, as shown in Listing 17.1. You can find this example on-line at
http://www.anadas.com/cgiunleashed/voting/greenegg.html

Listing 17.1. greenegg.html-The HTML half of a simple voting booth system.
<HTML>
<HEAD><TITLE>Are green eggs and ham meant for you?</TITLE></HEAD>
<BODY>
<H2>Funny Foods Grillhouse Customer Survey</H2>
<P>
Funny Foods is conducting a survey of our restaurent goers to test the
marketability of a new main course we're planning.
<P>
<FORM METHOD=GET ACTION=exe/greenegg.cgi>
Do you like green eggs and ham?<BR>
<SELECT SIZE=2 NAME=disposition>
<OPTION SELECTED VALUE="dislike">I do not like them, Sam I am</OPTION>
<OPTION VALUE="like">Yes, I'll try them, Sam I am</OPTION>
</SELECT>
<P>
If yes, what is the most you would pay for green eggs and ham at a
restaurant?<BR>
<SELECT NAME=pay>
<OPTION SELECTED>[select an amount]</OPTION>
<OPTION>$6</OPTION>
<OPTION>$9</OPTION>
<OPTION>$12</OPTION>
<OPTION>$15</OPTION>
<OPTION>$18</OPTION>
<OPTION>$21</OPTION>
</SELECT>
<P>
<INPUT TYPE=SUBMIT VALUE="Click here when finished">
</FORM>
</BODY></HTML>

This voting booth isn't particularly good. In my humble, objective, and removed opinion, it's a fine computer program. However, it doesn't live up to the two points I made previously. I'll discuss its technical merits and also examine how a better voting booth could be constructed. For your viewing delight, and to save you from booting up your browser, I've provided you with a screen shot of my simple voting booth page in Figure 17.1.
Figure 17.1 : The entry page of my simple voting booth.

Listing 17.2. greenegg.cgi-A simple voting booth CGI program.
#!/usr/bin/perl

#
# The following lines are "initializations" of one sort or another.
#
# Redirecting standard error to /dev/null is handy when dealing with
# the 'sendmail' UNIX command since sendmail has an odd habit of having
# errors it may encounter being piped to the HTTPd.  This is the only
# circumstance where I've seen standard out (STDOUT) and standard error
# (STDERR) get confused with each other.
#
chop($time = 'date');
$refpage = 'http://www.anadas.com/cgiunleashed/voting/greenegg.html';
$admin_email = 'rdice@anadas.com';
open(STDERR,"> /dev/null");

# -- start of my standard GET/POST method handler --
#
# At the end of this section of code, I'll have the %tokens associative
# array with the 'name' information from the submitting form as keys and
# 'value' information as values.  Hex-encoded special characters will be
# restored to their original characters.  Note that many browsers will
# return annoying DOS-style CRLF at line end in textarea boxes.  This
# code will _not_ strip ^M.  If this is desirable, use
#    $tokens{$field} =~ s/\cM/ /g;
#    $tokens{$field} =~ s/\n//g;
# as the last lines in the foreach loop.
#

if ( $ENV{REQUEST_METHOD} eq 'POST' ) {
   read(stdin,$input,$ENV{CONTENT_LENGTH});
} elsif ( $ENV{REQUEST_METHOD} eq 'GET' ) {
   $input = $ENV{QUERY_STRING};
} else {
   print "Content-type: text/html\n\n";
   print "This program doesn't support the <b>$ENV{REQUEST_METHOD}</b> httpd";
   print " request method.\n";
exit 1;
}
$input =~ tr/+/ /;
@fields = split(/\&/,$input);
$input = '';
foreach $i (@fields) {
   ($field,$data) = split(/=/,$i);
   $field =~ s/%(..)/pack("c",hex($1))/ge;
   $data =~ s/%(..)/pack("c",hex($1))/ge;
   $tokens{$field} = $data;
}
@fields = ();            # delete the @fields array

# -- end of my standard GET/POST method handler --

#
# The next 3 lines trap errors.  The first defuses improper accesses to
# greenegg.cgi -- this  CGI program -- while the other two trap for
# illogical form submissions.  The format is:
#    goto subroutine if error condition
# The various subroutines print to the HTTPd and then abort the program.
#
&refpage_error if $ENV{HTTP_REFERER} ne $refpage;
&dislike_error if ( ($tokens{'disposition'} eq 'dislike') &&
   ($tokens{'pay'} ne '[select an amount]') );
&like_error if ( ($tokens{'disposition'} eq 'like') &&
    ($tokens{'pay'} eq '[select an amount]') );

#
# To reach this point, there must be no input errors in the program.
# Now, open the database file of historical responses
#
open(DB,"greenegg.dat");
chop(@dataline = <DB>);
close(DB);

#
# parse each line, update as appropriate (Didn't like or Liked and
# pay amount)
#
foreach ( @dataline ) {
   ($name,$count) = split(/:\t/);
   if ( (($name eq 'Didn\'t like') && ($tokens{'disposition'} eq 'dislike'))
      || (($name eq 'Liked') && ($tokens{'disposition'} eq 'like'))
      || ( $tokens{'pay'} eq $name ) ) {
      $count += 1;
      $_ = join(":\t",$name,$count);
   }
}

#
# write new version of data to file
#
open(DB,"> greenegg.dat");
foreach ( @dataline ) {
   print DB "$_\n";
}
close(DB);
chmod 0660, "greenegg.dat";

#
# Construct a formatted email to send to the administrator of the Web site.
# The email is composed of 4 sections: The header (parsed by sendmail),
# environment info, info regarding the immediate form submission, and
# historical submission info.
#
open(EM,"| /usr/sbin/sendmail -t");
print EM <<END;
To: $admin_email
From: "Funny Food Form"
Subject: Submission of Funny Food Form

The Funny Food Grillhouse Web questionnaire has just received a submission!

Time of Submission: $time
Using Browser:      $ENV{HTTP_USER_AGENT}
From Host:          $ENV{REMOTE_HOST}

END
if ( $tokens{'disposition'} eq 'like' ) {
   print EM "The respondant likes green eggs and ham and would be willing ";
   print EM "to pay $tokens{'pay'} for a green eggs and ham main course.\n";
} else {
   print EM "The respondant doesn't like likes green eggs and ham...",
   " yet.\n";
}

print EM "\nHistorical Responses\n";
print EM "--------------------\n\n";
foreach ( @dataline ) {
   print EM "$_\n";
}

close(EM);

#
# Jump to a subroutine which will send formatted HTML to the HTTPd.  The
# message thanks the user for the form submission, including a report
# on their submission information and also historical information.
#
&thank_user;

exit 0;

sub refpage_error {
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Improper refering page!</TITLE></HEAD>
<BODY>
<P>
The page which invoked this CGI program was not
<A HREF=$refpage><B>$refpage</B></A>.  Please re-submit this form using
that page.
</BODY></HTML>
END
   exit 1;
}

sub dislike_error {
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Improper Input!</TITLE></HEAD>
<BODY>
<P>
If you don't like green eggs and ham then it doesn't make sense to say
you'd buy them for ANY price.  Please press the BACK button on your browser and re-enter your reply.
</BODY></HTML>
END
   exit 1;
}

sub like_error{
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Improper Input!</TITLE></HEAD>
<BODY>
<P>
Since you said you like green eggs and ham it is required that you provide
a price you'd buy them at.  Please press the BACK button on your browser
and re-enter your reply.
</BODY></HTML>
END
   exit 1;
}

sub thank_user {
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Thank you!</TITLE></HEAD>
<BODY>
<P>
Thank you for taking the time to fill out Funny Food's questionnaire.  Your
input has been added to the database.  Now, you can see how your replies
stack up against what other people have submitted:
END
   print "<P>You said that you <B>$tokens{'disposition'}</B> green eggs",
   " and ham.\n";
   if ( $tokens{'disposition'} eq 'like' ) {
      print "<P>You would pay as much as <B>$tokens{'pay'}</B> for a " ,
      "green eggs and ham main course.\n";
   }

   print "<H3>Historical Information</H3><HR><PRE>\n";

   foreach ( @dataline ) { print "$_\n"; }
   print "</PRE>\n";
   print "</BODY></HTML>\n";
}

This code handles the fairly simple form data generated by greenegg.html. The specific actions of this voting booth program are as follows:
  1. Get and decode the form data.
  2. Check for errors. If any are found, display an error message to the Web and abort the voting process.
  3. Update the database file with the newly submitted information.
  4. Compose an e-mail containing information on the current submission and send it to both the voter and the CGI system maintainer.
  5. Output a message thanking the user for voting to the Web. This message contains a report on both the current vote submission and votes in the past. (as shown in Figure 17.2).
Figure 17.2: The message a user receives after voting. The historical information is output without parsing at the bottom of the page.
This example has managed to capture the essentials of a voting booth. The process asks your voting/polling-style questions, and it generates a report that contains historical information regarding all votes submitted to date. Still, I find that the "feel" produced by this voting booth to be lacking. Yes, the questions are phrased in a vote-like way, but really there isn't much difference between this voting booth and a page that asks you whether or not you'd like to purchase a charity gift-basket and how much you'd like to pay for it-yet this example would be considered an on-line ordering form.
Usually, there would be no utility in providing users with a report of how many other people had bought charity baskets and a payment breakdown. So in this sense, Funny Foods has managed to create a bona fide voting booth. The display of the voting booth is fairly primitive, though, and it's questionable as to how much useful information can be gleaned from this report.

Bad Voting Booth, Good Code-Technical Merits of greenegg.cgi

I've already talked about how I feel that greenegg.cgi falls short as a voting booth. Of course, I did this on purpose; my hope is that you'll learn how to build a better voting booth from my (intentional) mistakes. However, one aspect of greenegg.cgi I will not compromise is its coding. Bad philosophy can be examined, but bad code should never be duplicated.
Having said that, I will state that there is one aspect of this code that could be improved upon in theory: modularity. What I refer to as my standard GET/POST method handler could be put in a subroutine, called as &subroutine_name and defined by sub subroutine_name { ... }. Even better, it could be encapsulated as a subroutine in an external file. Within the Perl program, the subroutine would still be called as &subroutine_name, but its definition would be within another file. The Perl interpreter/compiler would be instructed to find this file by placing the following line at the top of your Perl program, immediately following #!/usr/bin/perl:
require "external_file_name.pl";
In other words, I could have re-created cgi-lib.pl. This is a very popular package that does essentially the same job as my standard GET/POST handler, but it also has a lot of other useful CGI-handling features. This Perl library file can be found at
http://www.bio.cam.ac.uk/cgi-lib/

Caution
Please don't take this observation as an unmitigated endorsement! Though cgi-lib.pl is very popular, it does have its detractors. For a critical analysis of cgi-lib.pl, you can go to

http://www.perl.com/perl/info/www/!cgi-lib.html
For more modularity, the section within the main portion of the program that handles the creation of the e-mail could also be brought within a subroutine.
Now, let's look at the positive points of greenegg.cgi:

Use of Associative Arrays for the Handling of GET/POST Method Data

Perl's associative array feature allows data to be arranged in an array that is indexed by character strings rather than integers. This lets the programmer easily preserve the name-value pair structure that forms pass to CGI programs through GET and POST. If you wanted to keep this structure without associative arrays, as you might if you programmed in C, you could build parallel arrays and supply a backwards-indexing scheme. Me, I'll stick to Perl.

Caution
Associative arrays have a slight amount of difficulty when presented with name/value pairs generated by <INPUT TYPE=chECKBOX> or <SELECT MULTIPLE>. These sorts of form elements can create several name-value pairs that share the same name. Using the standard handler I provided, those memory locations in the array that would all be indexed by the same name will be overwritten with the newest value. Schemes to avoid this problem can be built as best suits the situation at hand.

Immediate Error Trapping Using the Statement Modifier Form of if

Programmers who are familiar with C will easily recognize the meaning of the following code:
if (comparison) {
   statement 1;
   ...
   statement n;
}
This structure is also perfectly valid in Perl, too. However, a simple C if structure could be written as follows:
if (comparison) statement;
This is not the case in Perl; Perl doesn't allow dangling conditionals. The Perl equivalent of this code would be
if (comparison) { statement; }
To "make up" for this (non) deficiency, Perl has a statement-modifier if structure:
statement if comparison;
(statement) if comparison;
Many people who migrate to Perl from C find this style of if structure somewhat disconcerting the first time they see it. Many of those people eventually learn to love it, though. I am a member of both these sets. It's a clear, concise, and intuitive way of writing an if statement.

Slurping Data with @array = <FILEHANDLE>;

In more traditional languages, one would write a loop that would read a file line by line until EOF was encountered. This can certainly be done in Perl, as well:
while ( $line = <FILEHANDLE> ) {
   @array[$i++] = $line;
}
This sort of loop is essential in Perl if you wanted to do more than just read lines into an array; for example, if you wanted some sort of if (condition) to decide whether or not to include $line in the array or not. But if not, you can accomplish the same in less code and less run-time with
@array = <FILEHANDLE>;

Note
In the case of a file attached to a filehandle, it's easy to see where the <FILEHANDLE> will stop being read into the array-at the end-of-file. However, you can also use the default standard input filehandle, STDIN, in this context. I'm not sure you'd want to, though. How does @array know when EOF has been reached? When standard input encounters a
CTRL-d EOF character. Intuitive, no?

Use of Output Filter to Provide stdin to /usr/sbin/sendmail and sendmail -t Flag

E-mail gateways are often used in CGI programming because they are among the few standard UNIX tools that lend themselves to CGI development. The preferred UNIX options for sending e-mail composed by a CGI program are mail and sendmail. I prefer sendmail because it is the more powerful of the two. sendmail takes its data from stdin and sends the message after it reaches EOF or a line with single . on it.
One method of utilizing sendmail from Perl would be to write to-be-e-mailed data to a temporary file and then cat it and pipe the output to sendmail, as follows:
open(EMAIL,"> tempfile$$.txt");
print EMAIL "stuff to be emailed... la la la...\n";
close(EMAIL);
system("cat tempfile$$.txt | /usr/sbin/sendmail $tokens{'email'}");
system("rm tempfile$$.txt");
The $$ in the name tempfile$$.txt is a special variable in Perl that represents the process identification number (PID) of the Perl process. I do this so that tempfile isn't accidentally overwritten if two people invoke this CGI program simultaneously; there are two unique tempfiles with this method. This is a Good Idea that can be used in many applications. In this particular case, there are better ways to handle the situation, though.
The sendmail command finds its destination e-mail address from the associative array element $tokens{'email'}. You can assume that this variable is obtained from the user putting this value into his or her form submission. Without any safeguards, this is a Bad Thing. Consider the malicious hacker who provides the following "e-mail address" to your Web form:
noone@nowhere.net ; cd / ; rm -R *
The semicolons are shell metacharacters that signify the end of a UNIX command to the shell interpreter. Should any Webmaster be foolish enough to run their httpd as root, that example would delete the entire file system. While most Webmasters haven't set up their systems for this kind of disaster, lesser evils can be committed if unchecked user input is allowed to reach the shell.
This problem is avoided in my greenegg.cgi code in two ways. First, I avoid the intermediate tempfile and instead open a filehandle directly to the pipe that sendmail uses to receive standard input. Second, I use the -t option of sendmail that instructs it to parse standard input to look for an e-mail address rather than get the address from the command line. With this, no user input is brought to the attention of the shell at all.

Tip
If user input must go to the shell, then you might consider using the following Perl subroutine on it first:

sub shell_proof {

   local(@strings);
   @strings = @_;

   foreach $string ( @strings ) {
      $string =~
s/[\001-\011\013-\014\016-\037\041-\052\057\074-\077\133-\136\140\173-
377]//g;
   }
   @strings;
}
This code will remove shell metacharacters and binary characters from strings. Its syntax of use is
@string_list = &shell_safe(@string_list);

Use of print FH<<END; ... END Syntax When Outputting Formatted Sections

Perl has a strong built-in facility for printing the formatted output of a large number of consecutive lines of text.
$things = 'variables';
print <<END_DESCRIPTOR;
Now you type
what you want to
on as many lines as you'd like and you can even include
$things which will be interpolated to their values.
END_DESCRIPTOR
This syntax is taken from UNIX Bourne shell programming, where it is very useful when writing batch jobs. I find this feature of Perl programming to be particularly useful when writing HTML within Perl.

Note
Emacs, the ultimate UNIX-based text editor for hacking, has built-in modes for dealing with many different programming languages: C, C++, FORTRAN, Lisp, Prolog, and Perl to name a few. Among other things, these modes will automatically format your code as you type it. This is useful for discovering certain sorts of errors before you even touch the compiler. Unfortunately, emacs' Perl mode has a tough time figuring out how to format your source following a print <<END_DESCRIPTOR; statement. We can hope that this bug will one day be fixed.

A Voting Booth Wish List

I think that my main complaint of greenegg.cgi is that it didn't go far enough. Let's all try closing our eyes and clicking our heels together three times, thinking about what we'd most want in a voting booth CGI system...
  • The voting booth would recognize individual users, each having a unique e-mail address.
  • Users could not only vote but re-vote should they change their minds in the future.
  • There would be an extensible database of vote-able objects.
  • There would be different classes of objects within this database.
  • The reports that were generated by the voting booth were meaningful.
Of course, I'm not bringing these points up idly. The next 10 or so pages is the Perl source for vote.cgi, a voting booth program that demonstrates all these possibilities.
Figure 17.3 shows the vote.cgi entry page showing the various options a user would have when reaching it: registering as a new user, logging in as a current user, and viewing the vote patterns of all users. I have added my e-mail address into the field that shows how a new user is registered.
Figure 17.3: The vote.cgi entry page.
I've really put myself on the line with this example. vote.cgi gives the whole world an opportunity to tell me what they think about my taste in music. (Hey, cut me some slack. I happened to have a tab-delimited ASCII file of my music handy, and I needed to give myself a voting booth-come-database project, so....) The Perl source code for vote.cgi is shown in Listing 17.3.

Listing 17.3. vote.cgi-A complex voting booth CGI program.
#!/usr/bin/perl

#
# This program written by Richard Dice of Anadas Software Development
# as part of the Sams Net "CGI Programming Unleashed" book.  The author
# intends this code to be used for instructional purposes and not for
# resale or commercial gain.
#
# Any questions or comments regarding this program are welcome.  You
# may contact the author by Internet email: rdice@anadas.com
#

#
# minor set-up --> seed the random number generator (used in issuing
# passwords) and define the $ref_url, the only URL that this program
# will accept FORM-based submissions from.  I redirect standard error
# so that sendmail won't crash the system if it can't find an address
#
$ref_url = 'http://www.anadas.com/cgiunleashed/voting/exe/vote.cgi';
srand;
open(STDERR,"> /dev/null");
$admin = 'rdice@anadas.com';

# -- start of my standard GET/POST method handler --

if ( $ENV{REQUEST_METHOD} eq 'POST' ) {
   read(stdin,$input,$ENV{CONTENT_LENGTH});
} elsif ( $ENV{REQUEST_METHOD} eq 'GET' ) {
   $input = $ENV{QUERY_STRING};
} else {
   print "Content-type: text/html\n\n";
   print "This program doesn't support the <b>$ENV{REQUEST_METHOD}</b>",
   " httpd request method.\n";
   exit 1;
}
$input =~ tr/+/ /;
@fields = split(/\&/,$input);
$input = '';
foreach $i (@fields) {
   ($field,$data) = split(/=/,$i);
   $field =~ s/%(..)/pack("c",hex($1))/ge;
   $data =~ s/%(..)/pack("c",hex($1))/ge;
   $tokens{$field} = $data;
}
@fields = ();    # delete the @fields array

# -- end of my standard GET/POST method handler --

#
# MULTISTATE DETERMINATION
# ------------------------
# Switch to one of the possible actions given the value of the 'submit'
# name
#
# * if the submitting URL isn't this URL and yet there is form input,
#   likely someone is trying to hack the system
# * if no form input is encountered, it's the first access to this page
# * view historical statistics
# * allow a registered user to vote
# * process form following voting
#
if ( ($ENV{HTTP_REFERER} ne $ref_url) && defined(%tokens) ) {
   &referer_error;
}
&entry_page if !defined($tokens{'submit'});
&new_user($tokens{'email'}) if $tokens{'submit'} eq 'Register a New User';
&view_stats if $tokens{'submit'} eq 'View Historical Statistics';
&reg_user if $tokens{'submit'} eq 'Registered Users Proceed';
&process_votes if $tokens{'submit'} eq 'Submit these Votes';

exit 0;

#
# Some URL other than $ref_url attempted to make a form submission
#
sub refer_error {
   print <<END;
<HTML><HEAD><TITLE>Refering page error</TITLE></HEAD>
<BODY>
<H2>Refering Page Error</H2>
<P>
The page which is being used to access this CGI program is not permitted
to invoke the program.  Please use this program via:
<P>
<A HREF=$ref_page>$ref_page</A>
END
}

#
# Entry without form submission : throw up an HTML page which presents
# a user with 3 area options.  Note that a different VALUE is associated
# with each INPUT TYPE=SUBMIT button.  The Multistate switcher in the
# main of this program looked for that value to determine which state to
# employ
#
sub entry_page {
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Welcome to Richard's Vote-able Music Database</TITLE></HEAD>
<BODY>
<H3>Choose one of the following options for interacting with Richard's
Vote-able Music Database:</H3>
<HR>
<FORM METHOD=POST ACTION=$ref_url>
<INPUT TYPE=SUBMIT NAME="submit" VALUE="View Historical Statistics">
</FORM>
<HR>
<FORM METHOD=POST ACTION=$ref_url>
<TABLE>
<TR><TD VALIGN=TOP ALIGN=LEFT COLSPAN=1><B>Email Address:</B></TD>
<TD VALIGN=TOP ALIGN=LEFT COLSPAN=1><INPUT TYPE=TEXT NAME="email"></TD>
<TD VALIGN=TOP ALIGN=LEFT COLSPAN=1>
<INPUT TYPE=SUBMIT NAME="submit" VALUE="Register a New User">
</TD></TR></TABLE>
</FORM>
<HR>
<FORM METHOD=POST ACTION=$ref_url>
<TABLE>
<TR><TD VALIGN=TOP ALIGN=LEFT COLSPAN=1><B>Email Address:</B></TD>
<TD VALIGN=TOP ALIGN=LEFT COLSPAN=2><INPUT TYPE=TEXT NAME="email"></TD>
<TD VALIGN=CENTER ALIGN=LEFT COLSPAN=1 ROWSPAN=2>
<INPUT TYPE=SUBMIT NAME="submit" VALUE="Registered Users Proceed"></TR>
<TR><TD VALIGN=TOP ALIGN=LEFT COLSPAN=1><B>Password:</B></TD>
<TD VALIGN=TOP ALIGN=LEFT COLSPAN=1><INPUT TYPE=PASSWORD NAME="password"></TD>
</TD></TR></TABLE>
</FORM>
<HR>
</BODY></HTML>
END
}


#
# a user has asked for a new account.  Gives a fairly random 6 digit
# number to them as their password via email
# Also, creates an empty .cdd file -- this file will sort this users
# vote information
#
sub new_user {

   local ($email);
   ($email) = @_;

#
# Trap for no email address entry.  It's still very possible for someone
# to feed the program a non-existant email address, or a random string of
# characters for that matter, but it won't crash the program and it won't
# do the offending person any good, either, as their password will be
# provided to them by email anyhow
#
   $email =~ s/\s//g;    # eliminates all whitespace in email address
   if ( $email eq '' ) {
      print "Content-type: text/html\n\n";
      print <<END;
<HTML><HEAD><TITLE>No Email Address was entered</TITLE></HEAD>
<BODY>
<P>
Either nothing was entered into the email address field or only whitespace
characters were entered.  Return to the <A HREF=$ref_url>home page</A> of this
site to re-start the process.
</BODY></HTML>
END
   } else {

#
# create a password of 6 mostly random digits... check to see if a data file
# with that password already take exists... if so, add one to the password
# and check to see if that exists... repeat until available password is found
#
      $pswd = rand;    # put a random number into pswd
      substr($pswd,$[,2) = ''; # remove 2 digits from its front
# to this number, add the number of seconds since 1970 & the current PID
      $pswd = $pswd + time + $$;
      reverse $pswd;    # reorder string back to front
      substr($pswd,$[+6) = ''; # take the first 6 characters
      while ( -e ($pswd . '.cdd') ) { $pswd++; } # checks for file Existance

#
# create the datafile with name "PASSWORD.cdd"
# Store email address and password as data fields
#
      $passfilename = $pswd . '.cdd';
      open(DF,"> $passfilename");
      print DF "Email Address\t$email\n";
      print DF "Password\t$pswd\n";
      close(DF);
      chmod 0660, $passfilename;

#
# Send email to new subscriber telling them what their password is
#
      open(EM,"| /usr/sbin/sendmail -t");
      print EM <<END;
To: $email
Cc: $admin
From: "Richard's Music Database Program"
Reply-To: "Richard Dice" <rdice\@anadas.com>
Subject: Welcome, new subscriber!

You are now a subscriber to Richard's Music Database.  This allows you to
vote on what you think of his music collection.

To access your account, go to:
   $ref_url

Email Address : $email
Password      : $pswd

Hope it's fun for you!
END
      close(EM);

#
# Output a message to the Web providing further instructions
#
      print "Content-type: text/html\n\n";
      print <<END;
<HTML>
<HEAD><TITLE>Welcome, New Registered User!</TITLE></HEAD>
<BODY>
<P>
<H3>Welcome, New Registered User!</H3>
<P>
You will be receiving an email shortly which will tell you your password.
Once you have that information, please go back to the
<A HREF=$ref_url>home page</A> and enter as a registered user.
</BODY></HTML>
END
   }
}

sub view_stats {

   $num_votes = 0;

#
# construct @line array of all cd.txt datafile lines which have voted-upon
# albums, and @shortlist array of all such lines which are also marked
# as being on the short list (that is, the 3rd datafield is a "*").
# Also, create running total of the number of votes recorded and the total
# 0-10 votes submitted
#
   open(CD,"cd.txt");
   while ( <CD> ) {
      chop;
      @field = split(/\t/,$_,5);
      if ( $field[3] != 0 ) {
         push(@line,$_);
         $vote_total += $field[4];
         $num_votes += $field[3];
         push(@shortlist,$_) if $field[2] eq '*';
      }
   }
   close(CD);
   if ($num_votes != 0 ) {
      $average = $vote_total / $num_votes;
   } else { $average = 0; }

#
# order both @line and @shortlist arrays by descending order of album
# average vote score
#
   (@line = sort by_average @line) if defined(@line);
   (@shortlist = sort by_average @shortlist) if defined(@shortlist);

#
# output reports to the Web -- first is total # of voters, then Average Vote,
# then table of @line-related information... all pretty straightforward stuff
#
   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Historical Voting Record</TITLE></HEAD>
<BODY>
<P>
Here is the historical record of all votes taken regarding Richard's Music.
<P>
END

   print "<B>Number of votes cast: $num_votes <BR>\n";
   printf("Average Score of Album: %5.2f </B>\n",$average);
   print "<H3>All Albums</H3>\n";
   print "<TABLE BORDER WIDTH=100%><TR>\n";
   print "<TR><TH>Artist</TH><TH>Album</TH><TH># of Votes</TH>",
   "<TH>Vote Ave.</TH></TR>\n";
   foreach ( @line ) {
      @field = split(/\t/,$_,5);
      print "<TD ALIGN=LEFT VALIGN=TOP>$field[0]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=TOP>$field[1]</TD>\n";
      if ( $field[2] eq '*' ) {

         $shortcut = &hash($field[0],$field[1]);
         print "<TD ALIGN=CENTER VALIGN=TOP COLSPAN=2><A HREF=\"#$shortcut\">",
         "See Short List</A></TD></TR>\n";
      } else {
         print "<TD ALIGN=LEFT VALIGN=TOP>$field[3]</TD>\n";
         printf("<TD ALIGN=LEFT VALIGN=TOP>%5.2f</TD></TR>\n",
         $field[4]/$field[3]);
      }
   }
   print "</TABLE>\n";

#
# generate "short-list" table... same as above, but also includes standard
# deviation in votes.  Also, contains <A NAME> information which is refered
# to by links in the standard list.  This hashing scheme of
# "ARTIST _ ALBUM" and then remove all characters not in the range a-z, A-Z,
# 0-9 and _ is used as a standard throughout this program to uniquely
# identify any album.
#

#
# create an array with the file names of all .cdd files within
#
   @cddfile = <*.cdd>;

   print "<H3>The Short List</H3>\n";
   print "<TABLE BORDER WIDTH=100%><TR>\n";
   print "<TR><TH>Artist</TH><TH>Album</TH><TH># Votes</TH><TH>Vote Ave.</TH>",
   "<TH>Std. Dev.</TH></TR>\n";
   foreach ( @shortlist ) {
      @field = split(/\t/,$_,5);

#
# Create the standard deviation for an entry in the short list.  This is
# done using the formula:
#     StDev = ( (1/n) * sigma(i=1,i=n,(VOTE_i - Average Vote)^2) ) ^ 1/2
#
# VOTE_i is determined by parsing each and every .cdd file and checking for
# references to the album currently being parsed for
#
      $stdev = 0;
      $n = 0;
      $average = $field[4]/$field[3];

      foreach $cddf ( @cddfile ) {
         open(CDD,$cddf);
         while ( <CDD> ) {
            chop;
            @shortline = split(/\t/);
            if ( ($shortline[0] eq $field[0]) &&
            ($shortline[1] eq $field[1]) ) {
               $stdev += ($shortline[2] - $average)**2;
               $n++;
            }
         }
         close(CDD);
      }
      $stdev = ($stdev / $n)**0.5;

      $shortcutname = &hash($field[0],$field[1]);
      print "<TD ALIGN=LEFT VALIGN=TOP><A NAME=\"$shortcutname\">$field[0]</A>",
      "</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=TOP>$field[1]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=TOP>$field[3]</TD>\n";
      printf("<TD ALIGN=LEFT VALIGN=TOP>%5.2f</TD>\n",$field[4]/$field[3]);
      printf("<TD ALIGN=LEFT VALIGN=TOP>%7.4f</TD></TR>\n",$stdev);
   }
   print "</TABLE>\n";

#
# Now, output a graph of how many votes an artist got... I'm not sure
# what the statistical significance of this would but, but it'll look cool,
# and also show how graphs can be produced
#
   print "<H3>Graph of Vote Points Per Artist</H3>\n<PRE>\n";
   $maxkeylength = -1;
   foreach ( @line ) {
      @field = split(/\t/,$_,5);
      $votes{$field[0]} += $field[4];
      $maxkeylength = length($field[0]) if length($field[0]) > $maxkeylength;
   }
   $maxvotes = -1;
   foreach ( values(%votes) ) { $maxvotes = $_ if $_ > $maxvotes; }
   foreach ( sort by_votes keys(%votes) ) {
      printf("%${maxkeylength}s  : ",$_);
      $i = 1;
      $numstars = 30 * $votes{$_} / $maxvotes;
      $numstars = int(++$numstars) if ( ($numstars - int($numstars)) >= 0.5);
      while ( $i <= $numstars ) {
         print "*";
         $i++;
      }
      print " $votes{$_} vote points\n";
   }
   print <<END;
</PRE>
<P>
Return to <A HREF=$ref_url>Home Page</A>
</BODY></HTML>
END
}

sub reg_user {

#
# - open user file corresponding to the email address / password pair provided
# - display error page if file can't be found or email/password don't match
# - create @userfile array containing each line in the .cdd data file
#
   $userfilename = $tokens{'password'} . '.cdd';
   &password_error if ( !(-e $userfilename) );
   open(UF,$userfilename);
   chop(@userfile = <UF>);
   close(UF);
   ($email,$passwd) = splice(@userfile,0,2); # remove 1st 2 entries
   ($discard,$keep) = split(/\t/,$email);
   &password_error if ( $tokens{'email'} ne $keep);
   @userfile = sort @userfile;

#
# Construct @line array of all entries and @shortlist array for shortlisted
# entries.  Then, sort these two arrays alphabetically
#
   open(CD,"cd.txt");
   while ( <CD> ) {
      chop;
      @field = split(/\t/,$_,5);
      push(@line,$_);
      push(@shortlist,$_) if $field[2] eq '*';
   }
   close(CD);
   @line = sort @line;
   @shortlist = sort @shortlist;

   print "Content-type: text/html\n\n";
   print "<HTML>\n<HEAD><TITLE>Here is your voting profile</TITLE></HEAD>\n" ,
   "<BODY>\n<H3>Voting Profile of user <I>$tokens{'email'}</I></H3>\n" ,
   "<P>To re-cast or remove votes, use the appropriate selection menus " ,
   " and submit the form.\n";

   print "<FORM ACTION=$ref_url METHOD=POST>\n";
   print "<INPUT TYPE=HIDDEN NAME=\"email\" VALUE=$tokens{'email'}>\n";
   print "<INPUT TYPE=HIDDEN NAME=\"password\" VALUE=$tokens{'password'}>\n";
   print "<INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"Submit these Votes\">\n";
   print "<INPUT TYPE=RESET VALUE=\"Reset form to its original values\">\n";
   print "<P><TABLE BORDER>\n";

   print "<TR><TH VALIGN=TOP NOWRAP COLSPAN=3>Short-listed Albums currently",
   " Voted Upon</TH></TR>\n";
   print "<TR><TH VALIGN=TOP NOWRAP>Artist</TH><TH VALIGN=TOP NOWRAP>Album",
   "</TH><TH VALIGN=TOP>Your Vote</TH>\n";

#
# The following loop goes through all entries in the userfile which
# correspond with short-listed entries in the album database
#
   foreach $userline ( @userfile ) {
      @field = split(/\t/,$userline);
      $shortflag = 0;
      foreach $shortentry ( @shortlist ) {
         @shortfield = split(/\t/,$shortentry,5);
         if ( ($shortfield[0] eq $field[0]) && ($shortfield[1] eq $field[1]) ) {
            $shortflag = 1;
            $shortentry = 'DONE';
            $userline = 'DONE';
         }
      }
      next if $shortflag == 0;
      print "<TR>\n<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[0]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[1]</TD>\n";
      $namehash = &hash($field[0],$field[1]);
      print "<TD ALIGN=LEFT VALIGN=CENTER><SELECT NAME=$namehash>\n";
      print "<OPTION>[remove vote]</OPTION>\n";
      $select{$field[2]} = ' SELECTED';
      for $i (0..10) {
         print "<OPTION$select{$i}>$i</OPTION>\n";
      }
      undef(%select);

      print "</SELECT></TD></TR>\n";
   }

    print "<TR><TH VALIGN=TOP NOWRAP COLSPAN=3>Other Short-listed Albums",
    "</TH></TR>\n";
    print "<TR><TH VALIGN=TOP NOWRAP>Artist</TH><TH VALIGN=TOP NOWRAP>Album",
    "</TH><TH VALIGN=TOP>Your Vote</TH>\n";

#
# do the same for each short-list item not already encountered, as marked
# by the DONE flag... notice that now I don't have to use any special
# code to determine what OPTION to mark as SELECTED
#
   foreach ( @shortlist ) {
      next if $_ eq 'DONE';
      @field = split(/\t/,$_,5);
      print "<TR>\n<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[0]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[1]</TD>\n";
      $namehash = &hash($field[0],$field[1]);
      print "<TD ALIGN=LEFT VALIGN=CENTER><SELECT NAME=$namehash>\n";
      print "<OPTION SELECTED>[no current vote]</OPTION>\n";
      for $i (0..10) {
         print "<OPTION>$i</OPTION>\n";
      }
      print "</SELECT></TD></TR>\n";
   }

   print "<TR><TH VALIGN=TOP NOWRAP COLSPAN=3>Long-listed Albums currently",
   " Voted Upon</TH></TR>\n";
   print "<TR><TH VALIGN=TOP NOWRAP>Artist</TH><TH VALIGN=TOP NOWRAP>Album",
   "</TH><TH VALIGN=TOP>Your Vote</TH>\n";

#
# do the same for already voted-upon long-listed entries... all short-listed
# entries will be listed as DONE so skip
#
   foreach $userline ( @userfile ) {
      next if $userline eq "DONE";
      @field = split(/\t/,$userline);
      $userline = "DONE";
      foreach $entry ( @line ) {
         @entryfield = split(/\t/,$entry,5);
         if ( ($entryfield[0] eq $field[0]) && ($entryfield[1] eq $field[1]) )
         { $entry = "DONE"; }
      }
      print "<TR>\n<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[0]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[1]</TD>\n";
      $namehash = &hash($field[0],$field[1]);
      print "<TD ALIGN=LEFT VALIGN=CENTER><SELECT NAME=$namehash>\n";
      print "<OPTION>[remove vote]</OPTION>\n";
      $select{$field[2]} = ' SELECTED';
      for $i (0..10) {
         print "<OPTION$select{$i}>$i</OPTION>\n";
      }
      undef(%select);

      print "</SELECT></TD></TR>\n";
   }
   print "<TR><TH VALIGN=TOP NOWRAP COLSPAN=3>Other Long-listed Albums",
   "</TH></TR>\n";
   print "<TR><TH VALIGN=TOP NOWRAP>Artist</TH><TH VALIGN=TOP NOWRAP>Album",
   "</TH><TH VALIGN=TOP>Your Vote</TH>\n";

#
# do the same for not voted-upon long-listed entries... all short-listed
# and voted-upon entries will be listed as DONE so skip... note, no fancy
# code to determine which OPTION to mark as SELECTED
#
   foreach $entry ( @line ) {
      next if $entry eq "DONE";
      @field = split(/\t/,$entry,5);
      next if $field[2] eq '*';
      print "<TR>\n<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[0]</TD>\n";
      print "<TD ALIGN=LEFT VALIGN=CENTER NOWRAP>$field[1]</TD>\n";
      $namehash = &hash($field[0],$field[1]);
      print "<TD ALIGN=LEFT VALIGN=CENTER><SELECT NAME=$namehash>\n";
      print "<OPTION SELECTED>[no current vote]</OPTION>\n";
      for $i (0..10) {
         print "<OPTION>$i</OPTION>\n";
      }
      undef(%select);

      print "</SELECT></TD></TR>\n";
   }

   print "</TABLE>\n";

   print "<P><INPUT TYPE=SUBMIT NAME=\"submit\" VALUE=\"Submit these",
   " Votes\">\n";
   print "<INPUT TYPE=RESET VALUE=\"Reset form to its original values\">\n";
   print "</FORM>\n";
}

sub process_votes {

#
# remove all entries from tokens which amount to no voting information
# for that hashed artist/album pair
#
   foreach ( keys %tokens ) {
      delete $tokens{$_} if $tokens{$_} eq '[no current vote]';
      delete $tokens{$_} if $tokens{$_} eq '[remove vote]';
   }

#
# read in old vote information for all albums from cd.txt file
#
   open(CD,"cd.txt");
   while ( $line = <CD> ) {
      chop $line;
      @field = split(/\t/,$line,5);
      $hash = &hash($field[0],$field[1]);
      $albums{$hash,'artist'} = $field[0];
      $albums{$hash,'album'} = $field[1];
      $albums{$hash,'shortlist'} = $field[2];
      $albums{$hash,'num_votes'} = $field[3];
      $albums{$hash,'vote'} = $field[4];
      push(@hashes,$hash);
   }
   close(CD);
   @hashes = sort @hashes;
#
# read in old vote information for this user from .cdd data file
#
   $userfile = $tokens{'password'} . '.cdd';
   open(UF,"$userfile");
   while ( $line = <UF> ) {
      chop($line);
      @field = split(/\t/,$line,3);
      next if (($field[0] eq 'Email Address') &&
         ($field[1] eq $tokens{'email'}));
      next if (($field[1] eq 'Password') &&
         ($field[1] eq $tokens{'password'}));

      $hash = &hash($field[0],$field[1]);
      $albums{$hash,'vote'} -= $field[2];
      $albums{$hash,'num_votes'} -= 1;
   }
   close(UF);

#
# re-write the album list reflecting the new votes
#
   open(CD,"> cd.txt");
   foreach $hash ( @hashes ) {
      print CD "$albums{$hash,'artist'}\t";
      print CD "$albums{$hash,'album'}\t";
      print CD "$albums{$hash,'shortlist'}\t";
      if (defined($tokens{$hash})) {
         $albums{$hash,'num_votes'}++;
         $albums{$hash,'vote'} += $tokens{$hash};
      }
      print CD "$albums{$hash,'num_votes'}\t";
      print CD "$albums{$hash,'vote'}\n";
   }
   close(CD);
   chmod 0660,"cd.txt";

#
# rewrite the .cdd data with the new vote information
#
   open(UF,"> $userfile");
   print UF "Email Address\t$tokens{'email'}\n";
   print UF "Password\t$tokens{'password'}\n";
   delete $tokens{'email'};
   delete $tokens{'password'};
   delete $tokens{'submit'};
   foreach $hash ( keys %tokens ) {
      print UF "$albums{$hash,'artist'}\t";
      print UF "$albums{$hash,'album'}\t";
      print UF "$tokens{$hash}\n";
   }
   close(UF);
   chmod 0660, "$userfile";

   print "Content-type: text/html\n\n";
   print <<END;
<HTML>
<HEAD><TITLE>Thank you for voting!</TITLE></HEAD>
<BODY>
<H3>You vote has been received.  Please go to the <A HREF=$ref_url>home
page</A> to view statistics.</H3>
</BODY></HTML>

END

}

#
# for use in sorting tabular output in View Statistics option
#
sub by_average {

   @line1 = split(/\t/,$a,5);
   @line2 = split(/\t/,$b,5);

   return ( ($line2[4]/$line2[3]) <=> ($line1[4]/$line1[3]) );
}

#
# for use in sorting bar graphs
#
sub by_votes {

   return $votes{$a} <=> $votes{$b};

}

sub password_error {

   print "Content-type: text/html\n\n";

   print <<END;
<HTML>
<HEAD><TITLE>Invalid Email / Password Pair</TITLE></HEAD>
<BODY>
That email address / password pair is invalid.  Please return to the
<A HREF=$ref_url>home page</A> and try again.
</BODY></HTML>
END

   exit 1;

}

#
# standard hashing scheme used throughout the program to make a hash string
# out of artist & album strings
#
sub hash {

   local($artist,$album);
   ($artist,$album) = @_;
   $artist .= " _ $album";
   $artist =~ tr/a-zA-Z0-9_//cd;

   return $artist;
}


#
# standard hashing scheme used throughout the program to make a hash string
# out of artist & album strings
#
sub hash {

   local($artist,$album);
   ($artist,$album) = @_;
   ($artist .= " _ $album") =~ tr/a-zA-Z0-9_//cd;

   return $artist;
}

The raw programming power and technology exhibited by this program isn't all that different from greenegg.cgi. Sure, there's a lot more of it, but the commands and techniques are pretty much the same. The major difference lies in the organization of data structures and external files. Also, I've managed to create a few more conceptual aspects.

Multistate CGI Programs-More Than Just a URL

With the greenegg.cgi example, I first included greenegg.html, the file used to invoke the CGI program. This isn't the way it has to be. Using the same &subroutine if (condition); structure that traps errors in greenegg.cgi, I have made vote.cgi a multistate CGI program.
The word "state" is thrown around quite a bit in the field of computer science, but I've never really seen a good definition of the term. So, I'll have to make one up myself: state is the property that tells a system that can assume many different forms which of those forms is the one to manifest.
What does this mean in the context of vote.cgi, though? Well, give it a try! Go to
http://www.anadas.com/cgiunleashed/voting/exe/vote.cgi
When you first enter the page, it presents you with a number of different options. If you then choose one, the form calls vote.cgi, but this time vote.cgi doesn't react by displaying the same entry page. The value of NAME in the <INPUT TYPE=SUBMIT NAME=whatever> tag provides vote.cgi with the information necessary to choose an appropriate state-displaying historical statistics, sending an e-mail with a generated password, and so on. Figure 17.4 shows the interpreted HTML output that vote.cgi produces when displaying historical voting statistics.
Figure 17.4: A portion of the screen showing historical voting information. Notice that the URL is the same as before.
Multistate CGI programs have ups and downs to them. Some of their useful features are that people must follow your chain of events to proceed to the "page" you want them in. Also, you can bundle a whole Web site into one file if you want to-keeping everything in one place can be handy. However, it's harder to upkeep HTML embedded in a CGI program, especially a compiled one.

Generating Meaningful Reports

The game of voting, on the Web as in the rest of life, will eventually reduce itself to playing with numbers. The problem becomes devising a way to make those numbers meaningful.

Sorting Lists in Perl

In vote.cgi, I have decided to show output in tabular form. The columns are artist, album, number of votes submitted, and average vote. The observant user of this list will notice that the album with higher vote point averages is on the top of the list, and that vote point averages descend from there. This is not a coincidence.
Sorting a list is one of the canonical problems of computer science. The methods people come up with on the spot are often quite bad and usually turn out to be a variant on either the bubble sort or the selection sort. What I mean by "bad" in this context is that they have an order n-squared time complexity. This is compsci mumbo-jumbo meaning that the amount of logical operations required to complete the process is directly proportional to the square of the number of elements in the list. Think of it this way: If there are 10 albums in my database that have to be reported on, the sort will take 10*10 = 100 units of time to finish. If there are 20 albums to be sorted, then it will take 20*20 = 400 units of time. This sort of geometric growth in the time it takes to solve a computer problem absolutely must be avoided in real-world applications where tens of millions of data elements might need to be sorted.
Fortunately, there are better options when it comes to sorting. Two algorithms, Mergesort and Quicksort, are both n*log(n) algorithms. This would mean that the 10 element list would take 10 new-and-improved arbitrary time units to complete, while the 20 element list would take 20*log(20) = 26 arbitrary time units. Note that the arbitrary time units I summon up here aren't the same as before, so we can't compare these two algorithms directly. However, we do notice that there is a factor of 4 difference between the 10 and 20 element lists in the first case, while there is only a factor of 2.6 in the second. n*log(n) algorithms make sorting large lists manageable.
Now for your wake-up call: In Perl, you don't have to sort things directly. Instead, you can take advantage of Perl's built-in n*log(n) sorting function, reasonably called sort.
@destination_array = sort by_criteria @source_array;
@source_array holds your list of elements you want to be sorted. @destination_array will receive the sorted list. Note that they can both be the same array.
by_criteria is a subroutine you must build that will provide the sort command with its basis of comparing two elements with each other. Its general structure is
sub by_criteria {

   $atemp = $a;
   $btemp = $b;

   (statements regarding $atemp and $btemp, ultimately producing $value, 
    a variable with an integer numeric value)

   return $value;
}
Note that you can name by_criteria whatever you find appropriate for the circumstance.
sort is a special case when it comes to passing information to its by_criteria function. While Perl subroutines usually receive arguments through the @_ special list variable, sort will pick two elements from @source_array and call them $a and $b. $atemp and $btemp aren't strictly necessary, but they can be very useful. Anything that is done within the array that changes $a and $b will change values within @source_array itself! The inclusion of by_criteria is optional; if it isn't supplied, sort will do its job using the standard ASCII collating sequence as its criterion.

Tip
I often find it difficult to predict which order I'm going to end up sorting an array in using sort. As often as not, I end up sorting the list in exactly the opposite way I was intending. To fix this, I could hunt through my statements and negate of the logic within, but more often, I just use return ($value * -1);.

Average and Standard Deviation-Statistics 101

Everyone is familiar with the concept of the simple average. For some reason, we all feel very comfortable assigning meaning to an average value. I have decided to perform the tabular sorts on the criterion of descending average vote.
When dealing with vast amounts of numerical data, we mere humans can be easily overwhelmed with the quantity of numbers involved, and we start to lose track of the meaning behind those numbers. Sometimes, we don't even know the meaning of the numbers in the first place! This situation has given rise to the science of statistics. Statistics, put simply, are numbers that are used to represent other numbers. A good statistic is one that allows us a greater understanding of the scenario at hand with a lesser amount of sheer numbers.
An average of a set of numbers is an example of a very popular statistic. Not only does it have an intuitive meaning associated with it, but there's a wealth of mathematical study that shows that averages do a very good job of representing a great deal of information in a compact form. However, determining the average of a set of numbers isn't enough. Consider the following two number sets, both with average 5: (5,5,5,5) and (0,0,10,10). Think of these sets as being representative of a score on a scale of 0 to 10. The first set would show us that the thing being measured is wholly average-there is unanimous consent that the thing being polled on is middle-of-the-road. The second set, also averaging to 5, tells an entirely different story: You either love it or hate it.
Representing these two sets with just an average would be misleading; in doing so, we would have lost a great deal of information in the process. To help preserve the information relating to the agreement within the set on the average, a new statistic is introduced: standard deviation.
The concept of standard deviation is almost as intuitive as that of an average. Essentially, once you've determined your average, the standard deviation can be calculated as the average distance between the average value and the values of the members of the set of data points. Stated mathematically, this is written as
s represents the standard deviation, n the number of data elements in the set, -y the average of the data set, and yi the ith data element. S is the summation symbol, mathematical shorthand for "add up everything to the left of me between the value of i that's below me to the value that's on top of me."
I have included the standard deviation of the average of votes for "short-listed" albums in the table reporting on those albums.
Using standard deviation, we see that the standard deviation, s, for the set (5,5,5,5) is 0, while for (0,0,10,10) it is 5. We interpret this to mean that there is no disagreement between the numbers in the first set and the average of the first set, while the average disagreement between the numbers in the second set and their average is 5.

Tip
There is an alternative (yet equivalent) procedure used to calculate standard deviation:
Depending on how your data is stored, this might be the better formula for your program, both in terms of simplicity of programming and speed of execution.

Visual Output-Creating Graphs

Creating a graph isn't all that mathematically intense. The issues to keep in mind are
  • Keeping the left margin of the graph aligned, given that your labels will be of different lengths.
  • Imposing an upper limit to the length of any given bar in the bar graph. To do this, first make a pass through all your data and determine which is the greatest element. Then, divide all your output by that number.
  • Making sure that you "round to the nearest unit" when outputting your bars. Simply relying on your loop structure to print out bar elements can leave you open to bars that are 9 units in length when they should be 10, because your loop has an integer index. This will cause the loop to ignore the fact that your data element was actually 9.9 units. You'll have to do this rounding by hand.
Apart from these things, there isn't very much to creating a horizontal bar graph. Creating a vertical bar graph is a bit more challenging because print statements are built to display horizontal lines of text. To get around this in the past, I have created two-dimensional arrays within my program and filled those arrays as appropriate with characters representing data. Then, I write an output routine that parses the array row-wise and displays those rows.

Tip
Graphical (rather than ASCII) bar graphs can be easily created thanks to features found in modern Web browsers. Here's the procedure:

  1. Create a number of single-pixel GIF images, each in a different color.
  2. Write code that determines how wide (x) and tall (y) you want your bars to be.
  3. Output to the Web statements of the form <IMG SRC=appropriate_pixel.gif BORDER=0 WIDTH=x HEIGHT=y>.
Now, you have a graphical bar graph that took next to no time to transmit across the Internet (single pixels aren't large image files) and could be either horizontal or vertical with equal ease.

Handling Data Internally and Externally

Imagine a world where computer memory is virtually free, fast as light, and doesn't disappear when you turn off your computer. In that world, there wouldn't be any need to make a distinction between internal and external memory. A hard drive? A CD-ROM? What are those? Memory is...just memory!
The world I describe would justifiably be called "Utopia," a word composed from the Latin meaning "no where." Compare this to where we live: Memory exists in internal and external states. Internal memory is fast, operating on a time scale that corresponds with CPU speed. It can also be accessed asynchronously by multiple systems or user processes. However, it's also expensive and vanishes when you turn off your computer. External memory, exemplified by a hard drive, is cheap and relatively permanent but painfully slow. It's also stored sequentially even if it can be read (more or less) randomly. If you are a programmer whose programs end up "swapping to virtual memory," you could end up a "marked man" in many computer labs-I speak from experience!
The difference in characteristics between internal and external memory is one of the great problems of computer science. The solutions are often no more than workarounds. Even in a program as simple as my voting booth example, vote.cgi, I had to create different data representations depending on whether the context is disk based or internal. This section discusses the structures I built and explains my thinking behind making the choices I did in their design.

External Data files

vote.cgi is an engine, but the accompanying data files are its fuel. Here are the first few lines of the main database file, cd.txt:
Alice Donut[tab]Donut Comes Alive[tab][tab]0[tab]0
Beastie Boys[tab]Check Your Head[tab]*[tab]1[tab]8
Beastie Boys[tab]Ill Communication[tab][tab]0[tab]0
Blind Melon[tab]Blind Melon[tab][tab]0[tab]0
Bowie, David[tab]Outside[tab][tab]0[tab]0
Bowie, David[tab]Sound + Vision[tab][tab]0[tab]
0
Bowie, David[tab]The Rise and Fall of Ziggy Stardust[tab]*[tab]1[tab]9
A line in cd.txt has five tab-separated fields: artist, album, shortlist status, number of votes, and total vote points. An * is used to flag a short-listed album. Note that I'm using [tab] to represent an actual tab character in the preceding code snippet. I hope this makes it easier to read.
In addition to this main data file, each voter is given an individual data file, named ######.cdd, where ###### is their random 6-digit password. A .cdd file will have the following sort of structure:
Email Address[tab]rdice@anadas.com
Password[tab]905430
Prince[tab]Purple Rain[tab]7
Yes[tab]90125[tab]5
Simon, Paul[tab]Graceland[tab]8
Talking Heads[tab]Remain in Light[tab]8
The first two lines of this file are header information. The following lines relate directly to the albums in cd.txt. The first two fields of any album-related line directly match those fields in cd.txt. The fields relating to the short-list and number of votes cast aren't needed in this file, as only one vote can be cast on any given album by an individual, and the short-list information in cd.txt is sufficient for vote.cgi to operate.
My main consideration when designing both external data file structures was human readability. The main reason behind this was I wanted to be able to test portions of my code without having the whole program available to me. For instance, I wanted to test the display mechanisms before I'd written the input routines. So, I had to edit the data files by hand, and I wanted files I would understand while doing that. Figure 17.5 shows what the voting booth looks like to a registered user who logs in as such. Essentially, they are presented with information on their current voting status and given the option to amend their previous votes, as well as cast new ones.
Figure 17.5: The registered user sectio of vote.cgi.

Internal Data Representation

In most instances, vote.cgi simply reads in cd.txt line by line and places it in an array called @line. I decided to call it @line because I think that there is a special ring to the statement foreach (@line) { ... }, which is used quite often throughout the program.
While reading in each line of cd.txt, vote.cgi performs a test to see if the third field (referenced by $field[2]) of the current line being read is a *. If so, that line is directly copied into @shortlist.
When arranging the ASCII bar graph of artists and their votes, I use an associative array with the name of the artist as the key field, and the value is the running total of the votes received by that artist across all their albums in my collection. This is easy to accomplish by parsing through @line and finding the values of each field in lines, using $field[0] to represent the artist name and $field[4] as the total votes for that artist's album. The code for this is very straightforward and yet surprisingly powerful:
foreach ( @line ) {
   @field = split(/\t/,$_,5);
   $votes{$field[0]} += $field[4];
}
The split command is told to perform its splitting of $_, the value the @line element being looked at in the current loop iteration, on tabs, represented by \t.
One last programming technique I'd like to comment on is my use of a one-way hashing function to create a key that can be used as a "shorthand" album identifier. When an element of @line is split on tabs into a @field array, $field[0] corresponds to the artist and $field[1] the album name. I have produced a function that will combine these two fields, slightly modified, into a single string. This hash is often useful when I require a simplified way of identifying an album uniquely. For instance, in the registered voter section, <SELECT> tags are used to allow a registered voter to vote on an album. The hash is used to create the <SELECT NAME=hash> specifier. The hash is used more elegantly in the routine that updates data files with new voting information as a way to identify which elements of @line need to be updated.

Summary

I think I've covered about all the theory there is to cover on voting booths in this chapter, and I've given two rather specific examples of how minor and extensive voting booth systems can get. Before I close this chapter, I want to leave you with a few examples of voting booths to be found on the World Wide Web.
Though I made quite a show of panning my first voting booth, I think a lot of that had to do with its lack of interesting subject matter. Two dirt-simple voting booths I quite enjoy are the WWWF Grudge Match and Horus' History Poll.
WWWF - http://www.cheme.cornell.edu/~slevine/
Horus' - http://www.kaiwan.com/~lucknow/pollbook/pollbk.html
The WWWF Grudge Match is a voting booth that asks your opinion on who would be the victor in some of the most unlikely contests imaginable: for instance, a battle to the death between "A Rottweiler vs. A Rottweiler's Weight in Chihuahuas." This is some seriously funny stuff.
The Horus' History Poll takes a more serious look at Web polling when it asks the question: What was the most important military battle in history? Though the question isn't difficult, the answers supplied are often thought provoking and insightful.
A slightly more extensive voting system can be found at
http://www.georgemag.com/cgi-unprot/poll.pl
This is the George On-Line Magazine Weekly Poll. The poll asks for your opinion on a number of topical subjects and keeps an extensive history of past replies, with some minor statistic analysis on the side.
Because 1996 is an American presidential election year, be on the look-out for an explosion of voting booths on this topic. If you see any really good ones, please let me know. My e-mail address is easy to find. Cheers!