A More Accessible Alternative to Graphical Capchas

Reading a blog post on comment spam has minded me to describe a solution that I have worked on.

The ability to send postcards from web sites would be nice, if it weren’t for the abuse that such resources get from spammers.

Unbeknown to most who are using graphical “captchas” (a form of Turing Test) in their web content, to prevent automated submissions, captchas are also a good way of saying “we don’t give a damn about all you people with a vision impairment, who are dyslexic or who use non-graphical browsers”.

To summarise the above, spammers suck, but so do graphical captchas. So, what is the solution? Is there a truly accessible way of determining whether you have a bona fide visitor, or a naughty robot? The WorldWide Web Consortium (W3C) has this to say on the issue.

I have a solution, which I applied to a client’s postcard page. It is not perfect, as it could present problems to people with cognitive disabilties. However, put in a context where people with cognitive disabilities are not likely to be in the first place, it has the makings of an accessible solution. Feel free to use it, but code provided in good faith without warranty or implied fitness for purpose, blah, blah, legalistic blah.

My solution, written in Perl, asks two random questions from a list. Both have to be answered correctly for the submission to be accepted. This should be easy to adapt to PHP for those who don’t follow the teachings of St Larry of Wall.

Setting up the Questions


my ($q1,$a1,$qtu1)=randq();
my ($q2,$a2,$qtu2)=randq();

…where $q1, $q2 are the questions that will be asked, $a1,$a2 are the random (possibly wrong, possibly right) answers to the question posed and $qtu1,$qtu2 are “question to use” – the numbers of the random question/answer sets themselves.

In our Form

I have removed most of the <div>s from the following code example, to improve legibility.


<fieldset><legend>Anti-Spam</legend>
<div>
To prevent our postcards from being used for 'spam' by naughty robots,
please answer the following questions. As questions are selected at
random, you may be asked the same one twice.
</div>
<label for="q1">$q1</label>
<select name="q1" id="q1">$a1</select>
</code><code>
<label for="q2">$q2</label>
<select name="q2" id="q2">$a2</select>

<input type="hidden" name="qtu1" value="$qtu1" />
<input type="hidden" name="qtu2" value="$qtu2" />

Checking our Answers

In the example below, we are using Perl’s standard CGI module. $q is our CGI instance. PHP people, this is like $_GET[‘my_variable’]. (or POST).


sub checkqs
{
my $chk1=randq($q->param('qtu1'),$q->param('q1'));
my $chk2=randq($q->param('qtu2'),$q->param('q2'));
unless ($chk1 && $chk2)
{
# page header stuff
start_page();
print "<p>Sorry - you did not answer the questions correctly. You can try again by selecting 'postcards' from the menu.</p>";
# page footer stuff
end_page();
}
}

The Random Question Subroutine


sub randq
{
my ($q,$a);
my @seq=(
[0,2,4,1,3],
[1,2,0,3,4],
[1,0,4,2,3],
[0,2,1,3,4],
[1,0,2,3,4],
[3,4,2,1,0],
[1,2,4,3,0],
[1,0,2,3,4],
[1,2,3,0,4],
[2,0,3,1,4],
);
my %questions;
$questions{0}{q}='Who is the president of the USA?';
$questions{0}{a}[0]='George W. Bush';
$questions{0}{a}[1]='Bill Clinton';
$questions{0}{a}[2]='Ronald Reagan';
$questions{0}{a}[3]='Richard M. Nixon';
$questions{0}{a}[4]='Jimmy Carter';
$questions{1}{q}='Who is Batman's sidekick?';
$questions{1}{a}[0]='Robin';
$questions{1}{a}[1]='The Joker';
$questions{1}{a}[2]='Spiderman';
$questions{1}{a}[3]='Batmobile';
$questions{1}{a}[4]='George W. Bush';
$questions{2}{q}='What is 2 plus 2?';
$questions{2}{a}[0]='4';
$questions{2}{a}[1]='2';
$questions{2}{a}[2]='22';
$questions{2}{a}[3]='0';
$questions{2}{a}[4]='1';
$questions{3}{q}='What day comes after Tuesday?';
$questions{3}{a}[0]='Wednesday';
$questions{3}{a}[1]='Monday';
$questions{3}{a}[2]='Thursday';
$questions{3}{a}[3]='Tuesday';
$questions{3}{a}[4]='Friday';
$questions{4}{q}='What is the capital of France?';
$questions{4}{a}[0]='Paris';
$questions{4}{a}[1]='New York';
$questions{4}{a}[2]='Lyons';
$questions{4}{a}[3]='Jacques Chirac';
$questions{4}{a}[4]='Tours';
$questions{5}{q}='Where does the sun rise?';
$questions{5}{a}[0]='In the East';
$questions{5}{a}[1]='At night';
$questions{5}{a}[2]='In the South';
$questions{5}{a}[3]='In the West';
$questions{5}{a}[4]='In the North';
$questions{6}{q}='The Great Pyramid may be found in...';
$questions{6}{a}[0]='Egypt';
$questions{6}{a}[1]='Rocks';
$questions{6}{a}[2]='Japan';
$questions{6}{a}[3]='Texas';
$questions{6}{a}[4]='North Pole';
$questions{7}{q}='Who is the president of the USA?';
$questions{7}{a}[0]='George W. Bush';
$questions{7}{a}[1]='Bill Clinton';
$questions{7}{a}[2]='Ronald Reagan';
$questions{7}{a}[3]='Richard M. Nixon';
$questions{7}{a}[4]='Jimmy Carter';
$questions{8}{q}='Thorogoods Apple Wines sells...';
$questions{8}{a}[0]='Fine apple wines and vintage ciders';
$questions{8}{a}[1]='Sheep';
$questions{8}{a}[2]='Beer';
$questions{8}{a}[3]='Pasta';
$questions{8}{a}[4]='Whisky';
$questions{9}{q}='Cider is made from...';
$questions{9}{a}[0]='Apples';
$questions{9}{a}[1]='Beer';
$questions{9}{a}[2]='Cheese';
$questions{9}{a}[3]='Potatoes';
$questions{9}{a}[4]='Chicken';
$questions{10}{q}='Apples are...';
$questions{10}{a}[0]='Fruit';
$questions{10}{a}[1]='Spiders';
$questions{10}{a}[2]='Cheese';
$questions{10}{a}[3]='A type of fish';
$questions{10}{a}[4]='Chickens';
$questions{11}{q}='Sharks live in...';
$questions{11}{a}[0]='The Sea';
$questions{11}{a}[1]='Apricots';
$questions{11}{a}[2]='Greenhouses';
$questions{11}{a}[3]='Cider bottles';
$questions{11}{a}[4]='Chickens';
# Do this if there are no arguments -
# in other words, we need new questions
unless ($_[0])
{
my $qtouse=int(rand 10);
$q=$questions{$qtouse}{q};
my $set=int(rand 9);
my @ord=@{$seq[$set]};
for my $row (@ord)
{
$a.="<option>$questions{$qtouse}{a}[$row]</option>";
}
return ($q,$a,$qtouse);
}
else
{
return($questions{$_[0]}{a}[0] eq $_[1] ? 1 : 0);
}
}

Sorry that WordPress has screwed up the code formatting!