Summary
In this article, we describe how to design and implement an online voting page with Mason, the powerful Perl based template management system for building dynamic web sites. We first design tables that store member information, voting results and etc. Then we implement some helper functions that manipulate the rows in these tables. These functions can be tested fully and independently outside Mason and Apache's environments. Next we write Mason powered components and HTML files that use these functions to generate the voting pages, process user input, display the results and etc.
Introduction
Sometimes you may need to create an online voting page for your group or mailing list. You may find some scripts from the Web. In this article, we not only provide source code but also describe in details how to design and implement a simple and secure voting page. In doing this, we hope it will be much easier for you to adapt the code to your requirements.
Nowadays email address is ubiquitous, so we can use it to identify our users. Many online groups or mailing lists allow you to export the member list from which you can build a member database easily.
When a user comes to your voting page and provides an email address, you can check your member database to see if the user exists in it or not. Because only the email address owner can read his/her email, you can generate a voting code based on it and send it to the associated email address so the user can use the voting code to authenticate himself/herself.
The voting code can be generated programmatically and doesn't incur any additional storage overhead. For example, for low security environment, you can just use as the voting code the first six characters of the MD5 of the user's email address and a secret that is known to yourself only.
With these ideas in mind, we can start designing the database tables. Then we can implement some functions that manipulate the data in these tables. These functions should be well tested outside Mason and Apache's environment. Only after these two steps are done, shall we proceed with design and implementation of the voting page which mostly glues together all the functions we implement earlier.
Table design
Table design is quite simple for the voting page. The first table stores the candidate's information. In addition to name and email address, we also need to assign each candidate a unique numeric ID so other tables can reference it easily. The second table stores all your group members' information. Similar to the candidate table, only name and email address are crucial. You also need to assign each of your members a unique numeric ID. The third table stores the voting results. It needs to have two fields minimally: Candidate ID and Voter ID.
To avoid spamming your members, you also need to have a request table that tracks when email containing the voting code is sent to a given email address. With this information, you can enforce how often you want to send the voting code to an email address, e.g., at most once every day. This table just needs to have three fields: The voter ID, the time the voting email is sent and the IP address that the request comes from.
If you are concerned that someone may do something mischievous, you can also add another table that tracks the voter's IP address. Later on you may detect something fishy if many votes come from the same IP address.
You can download the sample SQL file that creates the voting database and all the tables discussed earlier.
Implementation of supporting functions
After designing all the tables, we are ready to implement some supporting functions. We prefer this bottom-up approach so we can implement and test them fully before we actually build the pages. We can use either PHP or Perl. In this article, we start with Perl implementation. This is much simpler because of the availability of many quality packages from CPAN, the Comprehensive Perl Archive Network.
Using Class::DBI
The Perl library that interfaces with database is the Perl DBI. It is already quite easy to use. However, we can go a step further by using the Class::DBI package from CPAN which provides higher level of abstraction.
With Class::DBI, each table maps to a different class that derives from a common class. In the common class which derives from Class::DBI, you can define the database, user name and password that is shared by all the derivative class. The code looks like this:
package Voting::DBI;
use base 'Class::DBI';
Voting::DBI->connection("dbi:mysql:voting", "username", "password");
Here we assume you use MySQL and you need to change the database name, user name and password to match your local environment.
For individual tables, you define a class that links with the table's name and its columns. For example, for the candidates table, you can define the class as follows:
package Voting::Candidates;
use base 'Voting::DBI';
Voting::Candidates->table('candidates');
Voting::Candidates->columns(All => qw(id name email bio));
By default, the first column in the All list is the primary key.
If the primary key contains multiple columns, then you can use
Primary and Others to specify the key and non-key
columns. You can download
the
sample file that defines all the classes that are associated
with the tables for the voting page and rename it to
DBI.pm.
Once you define classes like the above, you get some handy functions such as
retrieve_all, search and etc so you don't need to
write any SQL statement for simple operations. For example, you can define a
Perl function that retrieves all the candidates' information as follows.
sub retrieveCandidates
{
my @candidates;
my @results;
@candidates = Voting::Candidates->retrieve_all;
foreach my $c (@candidates) {
my $rec;
$rec->{id} = $c->id;
$rec->{name} = $c->name;
$rec->{email} = $c->email;
push @results, $rec;
}
return \@results;
}
This function returns the reference to a list of references that store individual candidates.
Similarly, you can define the following supporting functions:
-
check_email_in_list: This function accepts an email address and return 1 if it is in the member list, 0 otherwise. -
get_candidate_name_by_id: This is a simple function that returns the candidate's name given a candidate ID. This will be used when you need to generate a list of candidates that voters can choose from for your HTML form. -
get_vote_code_for_email: This function takes a voter's email address, concatenate it with a secret and then do md5 on it. Afterwards, the first six characters of the resulting string (in hex format) is returned. This is the voting code which will be sent to the voter's email address so the voter can use it to vote. -
check_vote_code_for_email: This function accepts a voting code and an email address and check whether the voting code is for the email address. If so, it returns 1, otherwise 0. -
get_voter_id_by_email: This function searches the voters table and returns the voter ID for the given email address. If the email address is not found, 0 is returned because valid voter IDs start from 1. -
update_email_request: This function updates thevote_requesttable to record an entry saying that email containing the voting code has been sent to a given voter at a given time requested by a given IP address. With this table we can enforce how often the email can be sent to avoid spamming innocent users. -
check_email_sent_time_ok: This function checks whether we can send email to a given email address using thevote_requesttable discussed earlier. -
insert_voter_result: This function accepts a reference to a hash that contains voter's email, comment, IP address, and either a candidate ID or a reference to a list of candidate IDs. Because we want to allow voters to vote as many times as they want, before insertion we should also clear the voter's prior votes. For tracking purpose, we also record the voting time and the voter's IP as well. This function return 1 upon success and 0 otherwise. -
send_vote_code_to_email: This is the top level function that will be called from Mason. This functions accepts an email address. It first callscheck_email_sent_time_okto see if an email can be sent. If so, it will use theMail::Sendmodule from the Perl MailTools package to send email. Once it is done, it will update thevote_requestfor tracking purpose. Depending on how detailed you want the error message to be for users, you can return different values. For simplicity, the function just returns 1 for success and 0 for failure.
Please note that all these functions should be implemented such that it doesn't need to be called and tested from Web/Mason context so you can test them easily. This implementation strategy also makes them reusable in other contexts.
You can download
the sample file that contains all the utility functions
to support the online voting pages and rename it
to Utils.pm.
Implement the voting pages
From the top level, we can have two Web pages: One handles the voting, the
other handles the request for sending voting code. We name the first one
vote.html and the second one vote_request.html.
Because the request page is much simpler, we discuss it first.
The vote_request.html page actually needs to handle three states:
Initial page load, error and success. Initial page load is the one shown to
the user who visits it for the first time. Error is the one shown to the user
when he/she provides a non-existent email or an email has been sent recently.
Success is the one shown to the user when the voting code has been sent
successfully.
We organize the vote_request.html page as three parts. The first
part is the Mason <%init> block that is called first but it is put to the
end of the file. This one first checks whether a user has provided a valid
email address through the helper function sanitize_email_input. In
case the user just loads the page, there is no form submission and it just
returns the results in two variables. The variable $status is 1 if
it is valid, otherwise it is 0 and error message if any is stored in the
$msg variable. The special hash variable %ARGS
stores all the user's input. If the user provides a valid email address, then
the code will try to send an email. If it succeeds, then the variable
$confirm is set to 1, otherwise the error message is recorded in
the $msg variable.
So the first part in essence implements our business logic and fill out the
values in variables that can be used by the presentation page. The second part
is very simple: If the variable $confirm is 1, then it means the
voting code has been sent to the user's provided email address and we just need
to load the confirmation page which has little logic in it.
The second part is actually our presentation page. It is shown to the user if
she just visits the page or provides an invalid email address. It uses the
$msg to decide whether an error should be shown or not.
So far we have just described one possible way of organizing our page and Mason
code. An improvement is that you can actually move the
sanitize_email_input function to the Voting/Utils.pm
file so the vote_request.html can be much simpler. You can also
merge the first part and the third part together.
If you like to label patterns, you can actually regard the combination of the
first and the third parts as the controller and the second part as the view and
the functions implemented in the Voting/DBI.pm and
Voting/Utils.pm as the data model. We just expect the current
organization to be very simple and straightforward after all the explanations.
:)
You can download the source file for the page that a user can request the voting code for a given email address and the source file for the page that confirms with the user that the voting code has been sent for the details.
In addition to this, you will also need to configure apache to use Mason to handle requests to these pages.
First you need to load the Mason module for mod_perl environment. You can
create a separate file such as mason.conf and put it in your
apache's configuration directory. The file should contain the following:
PerlModule HTML::Mason::ApacheHandler
Then you need to tell where Mason component code resides.
PerlAddVar MasonCompRoot "main => /var/tmp/local/apache/htdocs/vote"
Most of the time, this should be the place where your HTML files that contain Mason blocks reside.
Next you specify the URI where Mason will be used to handle the requests.
<LocationMatch "/vote/.*(\.html|\.txt|\.pl)$">
SetHandler perl-script
PerlHandler HTML::Mason::ApacheHandler
</LocationMatch>
You don't want Mason to handle requests for all the files, e.g., image files in that directory otherwise it will slow down the process of downloading these files.
In your Mason configuration file, you can also preload all the dependent
libraries and functions that your code use so performance can be improved.
Please note that you need to put them all under the namespace of the package
HTML::Mason::Commands. The configuration block looks like the
following:
<Perl>
{
use lib qw(/directory/that/contains/Utils.pm/and/other/files);
package HTML::Mason::Commands;
use URI::Escape;
use Voting::Utils qw(
check_id_in_list
check_email_in_list
...
}
</Perl>
Please note that if you don't install your Perl module files in the standard
Perl lookup directories, you need to add the use lib qw(...) to
include that directory. You can find out Perl's default include directories by
running the following command:
perl -e '$,="\n"; print @INC;'
You can download the sample Mason configuration file for the voting pages for details.
We can implement the actual voting page similarly. The voting page needs to handle three states too: Initial page load, error and success. The first one is the initial page shown to a user who visits it for the first time. The second one is the error page a user gets when the user tries to vote but something goes wrong. For example, the user may fail to provide an email address, or the email address is not in the member list, or the voting code is incorrect and etc. The third one is the confirmation page shown when the user has successfully voted. It can include names of all the candidates that the user has just voted.
The file needs to be divided into three parts too. The first part is put into
the Mason's <%init> section. It basically validates user's
submission. If there is no user input or user's input is valid, it simply flows
through to the second part to show the page with optional error messages.
As part of the validation process, it also needs to retrieve the list
of candidates and check user's selections against it.
All the information a user submits through either HTTP GET or POST request
is available to Mason in the hash variable %ARGS. The function
sanitize_user_input does all the input filtering and validation.
If it succeeds, then the insert_voter_result function is called
which inserts the results into the table and also populates the
$ARGS{'candidates'} variable. This is later picked up by the
confirmation page to show the candidates that the user has just voted.
You can see the source file for the voting page and the voting confirmation page for details.
You may also notice that in the voting page we have one line that looks interesting:
<& /CandidateHelper.mas, %ARGS, candidates => $allcandidates &>
This actually calls a Mason component named CandidateHelper.mas
which accepts the %ARGS and @candidates variables as
input. As we have said earlier, the %ARGS hash contains user's
input. In it the $ARGS{'candidate'} variable contains either a
candidate ID (a Perl scalar variable) or the reference to a list of candidate IDs
(a Perl array variable). The @candidates variable comes from the dereferenced
value (array) that comes from the $allcandidates variable which is a
reference to all the candidates. The Mason component basically displays a list
of checkboxes with candidates' names next to them and remembers the user's
choices if there is anything wrong with the user's other input such as
unregistered email address and etc. You can download
the Mason component that
shows a list of candidates, rename it to CandidateHelper.mas
and copy it to the same directory as your other Mason enabled HTML files.
Because of limitation of our shared Web hosting environment, we cannot demo how the voting pages looks like. We will probably show how to implement the voting pages in PHP later.