package PgCommitFest::WebControl;
require Date::Calc;
require Template::Plugin::HTML;
use strict;
use warnings;
my %TYPE_ARGS = (
'_user' => {
'required' => qr/^(1|0)$/
},
'date' => {
'_base' => 'text',
},
'hidden' => {},
'integer' => {
'_base' => 'text',
'min_value' => qr/^\d+$/,
'max_value' => qr/^\d+$/,
},
'password' => {
'_base' => 'text',
},
'select' => {
'value_tag' => qr/^\w+$/,
'text_tag' => qr/^\w+$/,
'_base' => '_user',
},
'text' => {
'size' => qr/^\d+$/,
'maxlength' => qr/^\d+$/,
'_base' => '_user',
},
'textarea' => {
'rows' => qr/^\d+$/,
'cols' => qr/^\d+$/,
'_base' => '_user',
},
);
sub choice {
my ($self, $choice_list) = @_;
$self->{'value_key'} = 'id' if !defined $self->{'value_key'};
$self->{'text_key'} = 'name' if !defined $self->{'text_key'};
$self->{'choice'} = $choice_list;
return undef;
}
sub db_value {
my ($self) = @_;
$self->{'db_value'};
}
sub display_name {
my ($self) = @_;
return $self->{'display_name'};
}
sub display_name_html {
my ($self) = @_;
my $html = escape($self->{'display_name'});
if ($self->{'error'}) {
$html = "$html";
}
return $html;
}
my %ESCAPE = ('&' => '&', '<' => '<', '>' => '>', '"' => '"',
'\'' => ''');
sub escape {
my ($text) = @_;
$text =~ s/[&<>"']/$ESCAPE{$&}/ge;
return $text;
}
sub istype {
my ($self, $n) = @_;
return $self->{'istype'}{$n};
}
sub name {
my ($self) = @_;
return $self->{'name'};
}
sub new {
my ($class, $name, $type, $display_name, %args) = @_;
# Initialize object and list of type memberships.
die "type $type not recognized"
if !defined $type || !defined $TYPE_ARGS{$type};
my $self = bless {
'db_value' => {},
'display_name' => $display_name,
'error' => 0,
'istype' => {},
'name' => $name,
'type' => $type,
'value' => '',
}, $class;
my $istype = $type;
do {
$self->{'istype'}{$istype} = 1;
$istype = $TYPE_ARGS{$istype}->{'_base'};
} while (defined $istype);
# Parse arguments.
while (my ($argkey, $argvalue) = each %args) {
my $ctype = $type;
my $validator;
while (1) {
$validator = $TYPE_ARGS{$ctype}->{$argkey};
last if defined $validator;
$ctype = $TYPE_ARGS{$ctype}->{'_base'};
last if !defined $ctype;
}
if (!defined $validator) {
die "control $name has bad key $argkey\n";
}
elsif (!defined $argvalue) {
die "control $name, key $argkey has undefined argument value\n";
}
elsif ($argvalue !~ /$validator/) {
die "control $name, key $argkey has bad value $argvalue\n";
}
else {
$self->{$argkey} = $argvalue;
}
}
return $self;
}
sub render {
my ($self) = @_;
if ($self->{'istype'}{'text'}) {
my $size = 60;
my $maxlength = 200;
if ($self->{'istype'}{'date'} || $self->{'istype'}{'integer'}) {
$size = 10;
$maxlength = 10;
}
return sprintf
"",
$self->{'name'},
$self->{'istype'}{'password'} ? 'password' : 'text',
defined $self->{'size'} ? $self->{'size'} : $size,
defined $self->{'maxlength'} ? $self->{'maxlength'} : $maxlength,
$self->{'istype'}{'password'} ? '' : escape($self->{'value'});
}
elsif ($self->{'istype'}{'textarea'}) {
return sprintf
"",
$self->{'name'},
defined $self->{'rows'} ? $self->{'text'} : 6,
defined $self->{'cols'} ? $self->{'cols'} : 80,
escape($self->{'value'});
}
elsif ($self->{'istype'}{'hidden'}) {
return sprintf
"",
$self->{'name'},
escape($self->{'value'});
}
elsif ($self->{'istype'}{'select'}) {
my @html = (sprintf "";
return join('', @html);
}
else {
die "unable to render control $self->{'name'}, type $self->{'type'}";
}
}
sub set {
my ($self, $r, $default) = @_;
my $error;
# If the user is submitting the form (as indicated by the presence of the
# CGI parameter "go") or if the CGI parameter for this field is present
# in any case, we use that value. Otherwise, we fall back to the default
# provided by the user. If the default happens to be a reference, we
# assume it's a hash mapping control names to values.
my $value =
$r->cgi('go') || defined $r->cgi($self->{'name'}) ?
$r->cgi($self->{'name'})
: ref $default ? $default->{$self->{'name'}}
: $default;
# Basic sanitization of input. Text fields and text areas lose leading and
# trailing whitespace; text fields that are not text areas also have any
# string of whitespace characters smashed to a single space. Select
# controls can take only the values in the specified list.
if ($self->{'istype'}{'text'}) {
$value = '' if !defined $value;
$value =~ s/^\s+//;
$value =~ s/\s+$//;
$value =~ s/\s+/ /g;
}
elsif ($self->{'istype'}{'textarea'}) {
$value = '' if !defined $value;
$value =~ s/^\s+//;
$value =~ s/\s+$//;
}
elsif ($self->{'istype'}{'hidden'}) {
$value = '' if !defined $value;
}
elsif ($self->{'istype'}{'select'}) {
die "control $self->{'name'} has not specified choice list"
if !defined $self->{'choice'};
my $found = 0;
my $vk = $self->{'value_key'};
if (defined $value) {
for my $choice (@{$self->{'choice'}}) {
if (defined $choice->{$vk} && $choice->{$vk} eq $value) {
$found = 1;
last;
}
}
}
if (! $found) {
$value = @{$self->{'choice'}}
&& defined $self->{'choice'}[0]{$vk} ?
$self->{'choice'}[0]{$vk} : '';
}
}
my $db_value = $value;
# If the field is required, complain if the value is the empty string
# (unless "go" is not set, which means this is the initial form display
# and the user hasn't submitted it yet).
if ($self->{'required'} && $value eq '' && $r->cgi('go')) {
$error = 'is a required field.';
}
# If the field is a date, complain if it doesn't look like a valid date.
if ($self->{'istype'}{'date'} && $value ne '' && $r->cgi('go')) {
my $ok = 0;
if ($value =~ /^(\d{4})-(\d{1,2})-(\d{1,2})$/) {
my ($yy, $mm, $dd) = ($1, $2, $3);
if ($yy > 2000 && $mm >= 1 && $mm <= 12 &&
$dd >= 1 && $dd <= Date::Calc::Days_in_Month($yy, $mm)) {
$ok = 1;
}
}
$error = 'is not a valid date (use YYYY-MM-DD format).' if ! $ok;
}
# If the field is an integer, complain if it doesn't look like a
# valid integer or is out of range.
if ($self->{'istype'}{'integer'} && $value ne '' && $r->cgi('go')) {
if ($value !~ /^-?\d+$/) {
$error = 'must be an integer.';
}
elsif (defined $self->{'min_value'} && $value < $self->{'min_value'}) {
$error = 'is too small (the minimum value is '
. $self->{'min_value'} . ').';
}
elsif (defined $self->{'max_value'} && $value > $self->{'max_value'}) {
$error = 'is too large (the minimum value is '
. $self->{'max_value'} . ').';
}
}
# We store NULL for empty dates and integers.
if (($self->{'istype'}{'date'} || $self->{'istype'}{'integer'})
&& $value eq '') {
$db_value = undef;
}
# If any error was encountered, post it.
if (defined $error) {
$r->error($self->{'display_name'} . ' ' . $error);
$self->{'error'} = 1;
}
# Save and return the value.
$self->{'value'} = $value;
$self->{'db_value'} = $db_value;
return $db_value;
}
sub value {
my ($self) = @_;
$self->{'value'};
}
1;