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;