Question 6: Spreadsheet source
package CodeSheet::Spreadsheet;
use Moose;
use Try::Tiny;
use Digest::MD5 qw(md5_hex);
use Data::Dumper qw();
has 'qnum' => ( is => 'rw', isa => 'Int' );
has 'max_qnum' => ( is => 'rw', isa => 'Int', default => 100 );
has 'cell_data' => ( is => 'rw' );
has 'input_hash' => ( is => 'rw', isa => 'Str' );
has 'random_data' => ( is => 'rw' );
has 'last_hash' => ( is => 'rw', isa => 'Str' );
has 'formula' => ( is => 'rw', isa => 'Str' );
has 'solution' => ( is => 'rw', isa => 'Str' );
has 'message' => ( is => 'rw', isa => 'Str' );
has 'secret_token' => ( is => 'rw', isa => 'Str' );
my @cols = 'A' .. 'Z';
my @rows = 1 .. 100;
sub new_initial {
my $class = shift;
my $random_hash = md5_hex(rand());
my $self = $class->new(
qnum => 1,
input_hash => $random_hash,
);
$self->populate_cell_data;
$self->generate_formula;
return $self;
}
sub new_from_post {
my($class, $args) = @_;
my($self, $state);
try {
$state = $class->decode_token($args->{csrf_token});
$self = $class->new(%$state);
$self->populate_cell_data;
$self->generate_formula;
$args->{start_time} = $state->{timestamp};
$self->validate_solution($args);
$self = $class->new(
qnum => 0 + $self->next_qnum,
input_hash => $self->last_hash,
);
$self->populate_cell_data;
$self->generate_formula;
}
catch {
$self = $class->new(message => $_);
};
return $self;
}
sub csrf_token {
my $self = shift;
my $timestamp = sprintf('%04x', time() % 43200);
my $qnum = sprintf('%02x', $self->qnum);
my $plain_text = $timestamp . $qnum . $self->input_hash;
my $checksum = substr(md5_hex($plain_text), 0, 4);
my $key = int(rand(256));
return join '', sprintf('%02x', $key), $checksum, map {
sprintf('%02x', hex($_) ^ $key);
} $plain_text =~ m{(..)}g;
}
sub decode_token {
my($class, $token) = @_;
my($key, $checksum, $rest) = $token =~ m{\A(..)(....)(.*)\z};
$key = hex($key);
my $plain_text = join '', map {
sprintf('%02x', hex($_) ^ $key);
} $rest =~ m{(..)}g;
die "Invalid CSRF token. Did you do something naughty?\n"
if $checksum ne substr(md5_hex($plain_text), 0, 4);
my($timestamp, $qnum, $input_hash)
= $plain_text =~ m{\A(....)(..)(.*)\z};
return {
timestamp => hex($timestamp),
qnum => hex($qnum),
input_hash => $input_hash,
};
}
sub validate_solution {
my($self, $args) = @_;
die "Someone appears to have tampered with the formula.\n"
unless $self->formula eq $args->{formula};
die "'$args->{solution}' is not the correct solution.\n"
unless $self->solution eq $args->{solution};
my $now = time % 43200;
my $elapsed = ($now + 43200 - $args->{start_time}) % 43200;
if($elapsed > 5) {
die "You had the correct answer but took too long. "
. "You need to supply a solution within 5 seconds.\n";
}
return;
}
sub next_random_val {
my $self = shift;
my $data = $self->random_data || [];
if(!@$data) {
my $hash = md5_hex( $self->last_hash || $self->input_hash);
$self->last_hash($hash);
$data = [
map { hex($_) } $hash =~ m{(..)}g
];
$self->random_data($data);
}
return shift(@$data);
}
sub populate_cell_data {
my $self = shift;
my %data;
foreach my $r (@rows) {
foreach my $c (@cols) {
$data{"$c$r"} = $self->next_random_val();
}
}
$self->cell_data(\%data);
}
sub generate_formula {
my($self, $stage) = @_;
$stage //= $self->qnum;
if($stage > $self->max_qnum) {
return $self->secret_token("B75KTRJ6");
}
elsif($stage > 54) {
return $self->compound_addition();
}
elsif($stage > 45) {
return $self->list_max(2);
}
elsif($stage > 36) {
return $self->square_root();
}
elsif($stage > 27) {
return $self->simple_division();
}
elsif($stage > 18) {
return $self->simple_multiplication();
}
elsif($stage > 9) {
return $self->simple_subtraction();
}
else {
return $self->simple_addition($self->qnum == 1);
}
}
sub simple_addition {
my($self, $visible) = @_;
my $ref1 = $self->random_cell($visible);
my $ref2 = $self->random_cell($visible);
$self->formula("$ref1 + $ref2");
$self->solution(
$self->cell_value($ref1) + $self->cell_value($ref2)
);
}
sub compound_addition {
my($self, $visible) = @_;
$self->generate_formula($self->qnum - 54);
my $ref1 = $self->formula;
my $val1 = $self->solution;
my $ref2 = $self->random_cell($visible);
$self->formula("$ref1 + $ref2");
$self->solution($val1 + $self->cell_value($ref2));
}
sub simple_subtraction {
my($self) = @_;
my $ref1 = $self->random_cell();
my $ref2 = $self->random_cell();
my $val1 = $self->cell_value($ref1);
my $val2 = $self->cell_value($ref2);
if($val2 > $val1) {
($ref2, $ref1) = ($ref1, $ref2);
($val2, $val1) = ($val1, $val2);
}
$self->formula("$ref1 - $ref2");
$self->solution($val1 - $val2);
}
sub simple_multiplication {
my($self) = @_;
my $ref1 = $self->random_cell();
my $ref2 = $self->random_cell();
$self->formula("$ref1 * $ref2");
$self->solution(
$self->cell_value($ref1) * $self->cell_value($ref2)
);
}
sub simple_division {
my($self) = @_;
my $data = $self->cell_data;
my @ref_list1 = $self->random_keys();
my @ref_list2 = $self->random_keys();
foreach my $ref1 (@ref_list1) {
my $val1 = $data->{$ref1};
next unless $val1 > 1;
foreach my $ref2 (@ref_list2) {
next if $ref1 eq $ref2;
my $val2 = $data->{$ref2};
next unless $val2 > 1;
next unless $val1 > $val2;
if($val1 % $val2 == 0) {
$self->formula("$ref1 / $ref2");
$self->solution($val1 / $val2);
return;
}
}
}
return $self->simple_subtraction;
}
sub square_root {
my($self) = @_;
my $data = $self->cell_data;
my @ref_list = $self->random_keys();
foreach my $ref (@ref_list) {
my $val = $data->{$ref};
my $root = sqrt($val);
if($root == int($root)) {
$self->formula("SQRT($ref)");
$self->solution($root);
return;
}
}
return $self->simple_division;
}
sub list_max {
my($self, $count) = @_;
my @refs = ();
my $max = 0;
foreach (1 .. $count) {
my $ref = $self->random_cell();
my $val = $self->cell_value($ref);
push @refs, $ref;
$max = $val if $val > $max;
}
$self->formula("MAX(" . join(', ', @refs) . ")");
$self->solution($max);
}
sub random_cell {
my($self, $visible) = @_;
my($max_c, $max_r) = $visible ? (10, 10) : (25, 99);
my $c = $cols[ $self->next_random_val() * $max_c / 255];
my $r = $rows[ $self->next_random_val() * $max_r / 255];
return "$c$r";
}
sub random_keys {
my($self) = @_;
my $data = $self->cell_data;
my $key = $self->next_random_val();
return sort {
($data->{$b} ^ $key) <=> ($data->{$a} ^ $key)
|| $a cmp $b
} keys %$data;
}
sub cell_value {
my($self, $ref) = @_;
my $data = $self->cell_data;
return $data->{$ref};
}
sub next_qnum {
my $self = shift;
return sprintf('%03u', $self->qnum + 1);
}
sub is_error {
my $self = shift;
return $self->message ? 1 : 0;
}
sub data_as_html {
my $self = shift;
my @tr;
push @tr, "<tr><th></th>" . join('', map {
qq{<th class="col"><span>$_</span></th>};
} @cols) . "</tr>";
foreach my $r (@rows) {
push @tr, qq{<tr><th class="row"><span>$r</span></th>}
. join('', map {
my $val = $self->cell_value("$_$r");
qq{<td>$val</td>}
} @cols) . "</tr>";
}
return join "\n",
q{<table class="spreadsheet">}, @tr, "</table>";
}
1;