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 : ' ' 90 ); 91 } 92 print end_Tr; 93 } 94 # Handle write-ins. 95 print Tr(th(' '), 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 }