Listing 5. The vote program.
  0  #!/usr/bin/perl -Tw
  1  # -*- perl -*-

  2  use strict;
  3  use CGI qw(:standard *table *Tr *dl);
  4  use DBI;

  5  use constant REGISTRATION_CODE_LENGTH => 100;
  6  use vars qw(@CANDIDATES @PARTIES @OFFICES @CANDIDATE_NAME);
  7  $ENV{PATH} = '/bin';

  8  # connect to database
  9  my $DB = DBI->connect('dbi:mysql:CEA') or die "Can't connect: $DBI::errstr";

 10  ########################
 11  # create the page
 12  ########################
 13  print header,
 14    start_html(-title   => 'Indecision 2000',
 15               -bgcolor => 'white'
 16              ),
 17    h1({-align=>'CENTER'},
 18       img({-src=>'/icons/star.gif'}) x 3,
 19       'Indecision 2000',
 20       img({-src=>'/icons/star.gif'}) x 3);

 21  # load global variables
 22  get_globals();

 23  # If the VOTE button is pressed, validate and enter the ballot
 24  if (param('VOTE') && (my $registration = validate())) {
 25      enter_ballot($registration);
 26  }

 27  # Otherwise print the ballot
 28  else {
 29      generate_ballot();
 30  }

 31  # end of page
 32  print end_html;
 33  $DB->disconnect;

 34  exit 0;

 35  ###############################################################
 36  # get_globals() loads the @CANDIDATES, @CANDIDATE_NAME,
 37  # @PARTIES and @OFFICES globals from information in the
 38  # database.
 39  ###############################################################
 40  sub get_globals {

 41      # fetch the matrix of candidates, parties and offices
 42      my $query =<<END;
 43  SELECT candidate_id,first_name,last_name,party_name,
 44         office_name,candidate.party_id,candidate.office_id
 45    FROM candidate,party,office 
 46    WHERE candidate.office_id=office.office_id
 47    AND candidate.party_id=party.party_id
 48  END
 49  ;

 50      my $sth = $DB->prepare($query) or die "Can't prepare: ",$DB->errstr;
 51      $sth->execute;

 52      while (my($candidate_id,$first,$last,$party,$office,$party_id,$office_id) 
 53             = $sth->fetchrow_array) {
 54          $CANDIDATES[$party_id-1][$office_id-1] = $candidate_id;
 55          $CANDIDATE_NAME[$candidate_id] = "$first $last";
 56          $PARTIES[$party_id-1] = $party;
 57          $OFFICES[$office_id-1] = $office;
 58      }
 59      $sth->finish;
 60  }

 61  ###############################################################
 62  # generate_ballot(), voting_matrix(), validation_number() and vote()
 63  # create various parts of the page see by the voter
 64  ###############################################################
 65  sub generate_ballot {
 66      print start_multipart_form;
 67      voting_matrix();
 68      registration_number();
 69      vote();
 70      print end_form;
 71  }

 72  # This generates the table containing the ballot.
 73  sub voting_matrix {
 74      print img({-src=>'/icons/HandPointing.gif',-align=>'LEFT'}),
 75        h2('Step 1: Fill in your E-Ballot');

 76      print
 77        start_table({-cellspacing => 0,-border=>1}),
 78        Tr(th(''),th(\@OFFICES));

 79      for (my $party=0; $party < @PARTIES; $party++) {
 80          print start_Tr,th($PARTIES[$party]);

 81          for (my $office=0; $office < @OFFICES; $office++) {

 82              my $candidate = $CANDIDATES[$party][$office];
 83              print td({-bgcolor=>$office %2 ? 'white' : '#E0E0E0'},
 84                        $candidate ? radio_group(-name    => $office,
 85                                                 -value   => $candidate,
 86                                                 -labels  => {$candidate=>$CANDIDATE_NAME[$candidate]},
 87                                                 -default => '-',
 88                                              )
 89                                  : '&nbsp;'
 90                      );
 91          }

 92          print end_Tr;
 93      }

 94      # Handle write-ins.
 95      print Tr(th('&nbsp;'),
 96               td([map {radio_group(-name  => $_, -value => 'Write in:').
 97                         textfield(-name  => "writein $_",
 98                                  -value => '',
 99                                -override => defined param($_) && param($_)=~/^\d+$/
100                     )} (0..$#OFFICES)]
101                 )
102              ),
103        end_table;
104  }



105  # generate the field for entering voter registration number
106  sub registration_number {
107      print hr,
108        img({-src => '/icons/HandPointing.gif', -align => 'LEFT'}),
109        h2('Step 2: Enter your Registered Voter Code'),
110        blockquote(
111                   b('EITHER:'), 'Cut and paste the code here:',
112                   textarea(-name => 'registration_id', -rows =>4 , -cols => 70, -wrap => 'physical'), br,
113                   b('OR:'), 'Select voter registration file for upload here:', br,
114                   filefield(-name => 'registration_file')
115                 );
116  }

117  # generate the VOTE button
118  sub vote {
119      print hr,
120        img({-src => '/icons/HandPointing.gif', -align => 'LEFT'}),
121      h2('Step 3:','Cast your Ballot'),
122      blockquote(b(submit('VOTE')));
123  }


124  ###############################################################
125  # validate() validates the ballot to discourage fraud
126  ###############################################################
127  sub validate {
128      # first check that the voter registration field is filled out
129      return error('The voter registration ID field is missing.')
130        unless param('registration_id') || param('registration_file');

131      # check that the voter has voted for at least one office
132      return error('The ballot has not been filled out.')
133        unless grep {param($_) ne 'Write in:' || param("writein $_")} 0..@OFFICES-1;

134      # check that no office has more than one vote
135      for (0..@OFFICES-1) {
136          my @votes = param($_);
137          return error("You have voted for $OFFICES[$_] more than once.") if @votes > 1;
138      }

139      # recover the registration ID
140      my $registration_id;
141      if (my $fh = param('registration_file')) {
142          while (<$fh>) {
143              chomp;
144              next unless /--REGISTRATION-START--/../--REGISTRATION-END--/;
145              next unless /^\d+$/;
146              $registration_id .= $_;
147          }
148      }
149      $registration_id ||= param('registration_id');
150      $registration_id =~ s/\D//g;  # get rid of all non-digits
151      return error('Your registration code is the incorrect length.')
152        unless length $registration_id == REGISTRATION_CODE_LENGTH;

153      # check that this is a registered voter
154      my $sth = $DB->prepare('SELECT registration_used FROM registration WHERE registration_id=?')
155        or die "prepare registration: ",$DB->errstr;
156      my $rows = $sth->execute($registration_id);
157      return error("The registration code provided is not on the list of eligible voters.")
158        unless $rows > 0;

159      # check that registration ID has not already been used
160      my ($used) = $sth->fetchrow_array;
161      return error("That voter registration code has already been used.")
162        unless $used == 0;
163      $sth->finish;

164      return $registration_id;
165  }

166  ###############################################################
167  # enter_ballot() updates the database
168  ###############################################################
169  sub enter_ballot {
170      my $registration = shift;

171      # lock this registration number so that it can't be used again
172      $DB->do("UPDATE registration SET registration_used=1
173               WHERE registration_id='$registration'
174               AND registration_used=0")>0
175        or die "Can't update registration: ",$DB->errstr;

176      # generate a ballot ID
177      my $id = random_digits(100);

178      # prepare the SQL for regular and write-in votes
179      my $regular_vote = $DB->prepare("INSERT INTO tally VALUES('$id',?,?,NULL)")
180        or die "Can't prepare: ",$DB->errstr;

181      my $writein_vote = $DB->prepare("INSERT into writein VALUES('$id',?,?,NULL)")
182        or die "Can't prepare: ",$DB->errstr;

183      # begin user confirmation
184      print h2('Save this Information for your Records');
185      print start_dl;
186      for my $office (0..$#OFFICES) {
187          my $selection      = param($office);
188          my $writein        = param("writein $office");
189          my $candidate_name = $writein || $CANDIDATE_NAME[$selection] || '-none-';

190          # update database with the candidate's vote
191          if ($writein) {
192              $writein_vote->execute($office+1,$writein)   or die "can't update tally: ", $DB->errstr;
193          } elsif ($selection) {
194              $regular_vote->execute($office+1,$selection) or die "can't update tally: ", $DB->errstr;
195          }

196          # update confirmation page
197          print dt(b($OFFICES[$office])),dd($candidate_name);
198      }
199      print end_dl;
200      $writein_vote->finish;
201      $regular_vote->finish;

202      # show user his confirmation number
203      $id =~ s/(.{50})/$1\n/;
204      print h3('Ballot Confirmation Number'),pre($id);
205  }

206  ###############################################################
207  # utilities
208  ###############################################################

209  # generate some random digits for the ID
210  sub random_digits {
211      my $digits_desired = shift;
212      open(RAND, '/dev/urandom') or die "Can't open random number device: $!";
213      my $data;
214      read(RAND,$data,$digits_desired) or die "Can't read random bytes: $!";
215      my @digits = map {$_ % 10} unpack('C*',$data);
216      return join '', @digits[0..$digits_desired-1];
217  }

218  # all-purpose error message
219  sub error {
220      print p(font({-size=>'+2',-color=>'red'}, @_, br,
221                   'Please correct and try again.'));
222      return;
223  }