package HTML::FormsDj; use strict; use warnings; our $VERSION = '0.03'; use Data::FormValidator; use Data::FormValidator::Constraints; use Data::Dumper; use Carp::Heavy; use Digest::SHA; use Carp; our $_csrftoken; sub new { my($this, %param) = @_; my $class = ref($this) || $this; my $self = \%param; bless $self, $class; if (exists $self->{meta}->{fields} && exists $self->{meta}->{fieldsets}) { croak 'Either use meta->fields or meta->fieldsets, not both!'; } if (! exists $self->{field}) { croak 'No FIELDS hash specified!'; } if (! exists $self->{meta}) { $self->{meta} = {}; } if (! exists $self->{meta}->{fields} && ! exists $self->{meta}->{fieldsets}) { # generate them if the user doesn't bother $self->{meta}->{fields} = []; foreach my $field (sort keys %{$self->{field}}) { my $n = $field; $n =~ s/^(.)/uc($1)/e; push @{$self->{meta}->{fields}}, { field => $field, label => $n }; } } if (exists $self->{csrf}) { if ($self->{csrf} && ! $_csrftoken) { my $sha = Digest::SHA->new('SHA-256'); $sha->reset(); $self->{sha} = $sha; $_csrftoken = $self->_gen_csrf_token(); } } else { $self->{csrf} = 0; } return $self; } sub cleandata { my($this, %data) = @_; # construct validator structs my(@required, @optional, %input, %attrs, %constraints); $this->{isclean} = 0; if ($this->{csrf}) { if(! $this->_check_csrf(%data)) { # CSRF check failed, so we don't tamper with input # further. die and done. return (); } } if (exists $this->{dfv}) { # override all %input = %{$this->{dfv}}; } else { # generate dfv hash foreach my $field (keys %{$this->{field}}) { if($this->{field}->{$field}->{required}) { push @required, $field; } else { push @optional, $field; } $constraints{ $field } = $this->{field}->{$field}->{validate}; $input{ $field } = $data{ $field } || qq(); } } if (exists $this->{attributes}) { # there are dfv options, pass them as is %attrs = %{$this->{attributes}}; } if(! exists $attrs{required}) { $attrs{required} = \@required; } if(! exists $attrs{optional}) { $attrs{optional} = \@optional; } if(! exists $attrs{constraint_methods}) { $attrs{constraint_methods} = \%constraints; } # validate the input my $results = Data::FormValidator->check(\%input, \%attrs); if ($results->has_invalid or $results->has_missing) { # store errors for later output $this->{isclean} = 0; if ( $results->has_missing ) { foreach my $field ( $results->missing ) { $this->{missing}->{$field} = 1; } } if ( $results->has_invalid ) { foreach my $field ( $results->invalid ) { my $failed = $results->invalid( $field ); if (ref($failed) eq 'HASH') { $this->{invalid}->{$field} = join ', ', @{$failed->{$field}}; } else { $this->{invalid}->{$field} = join ', ', @{$failed}; } } } } else { if(exists $this->{clean}) { # call the custom clean() closure supplied by the user ($this->{isclean}, $this->{error}) = $this->{clean}(%{$results->valid}); } else { $this->{isclean} = 1; } } # store cleaned and raw data $this->{cleaned} = $results->valid; $this->{raw} = \%data; return %{$this->{cleaned}}; } sub clean { my($this) = @_; return $this->{isclean}; } sub error { my($this) = @_; if(exists $this->{error}) { return $this->{error}; } else { return qq(); } } sub _check_csrf { my ($this, %data) = @_; if (! exists $data{csrftoken}) { $this->{error} = 'CSRF ERROR: CSRF token is not supplied with POST data!'; return 0; } if (! exists $this->{'_csrf_cookie'}) { $this->{error} = 'CSRF ERROR: CSRF cookie is not set correctly(notexist)!'; return 0; } else { if(! $this->{'_csrf_cookie'} ) { $this->{error} = 'CSRF ERROR: CSRF cookie is not set correctly(undef)!'; return 0; } } my $posttoken = $data{csrftoken}; # hidden post var my $cookietoken = $this->{'_csrf_cookie'}; # cookie if ($posttoken ne $cookietoken) { $this->{error} = 'CSRF ERROR: supplied COOKIE csrftoken doesnt match stored csrf token!'; $this->{error} .= sprintf "
post: %s
cookie: %s", $posttoken, $cookietoken; return 0; } return 1; } sub as_p { my($this) = @_; my $html; $this->_normalize(); if ($this->{csrf}) { $html = $this->csrftoken(); } if (exists $this->{meta}->{fields}) { # just an array of fields foreach my $field( @{$this->{meta}->{fields}}) { $html .= $this->_p_field($field); } } else { # it's a fieldset foreach my $fieldset (@{$this->{meta}->{fieldsets}}) { my $htmlfields; foreach my $field (@{$fieldset->{fields}}) { $htmlfields .= $this->_p_field($field); } $html .= $this->_fieldset( join(' ', @{$fieldset->{classes}}), $fieldset->{id}, $fieldset->{legend}, $htmlfields ); } } return $html; } sub as_table { my($this) = @_; my $html; $this->_normalize(); if ($this->{csrf}) { $html = $this->csrftoken(); } if (exists $this->{meta}->{fields}) { # just an array of fields foreach my $field( @{$this->{meta}->{fields}}) { $html .= $this->_tr_field($field); } return $this->_table('formtable', $html); } else { # it's a fieldset foreach my $fieldset (@{$this->{meta}->{fieldsets}}) { my $htmlfields; foreach my $field (@{$fieldset->{fields}}) { $htmlfields .= $this->_tr_field($field); } $html .= $this->_table($fieldset->{id}, $htmlfields, $fieldset->{legend}); } } return $html; } sub as_is { my($this) = @_; $this->_normalize(); return $this->{meta}; } sub fields { my($this) = @_; if (exists $this->{meta}->{fields}) { return @{$this->{meta}->{fields}}; } else { return (); } } sub fieldsets { my($this) = @_; if (exists $this->{meta}->{fieldsets}) { return @{ $this->{meta}->{fieldsets} }; } else { return (); } } sub dumpmeta { my($this) = @_; my $dump = Dumper($this->{meta}); $dump =~ s/^\$VAR1 = / /; return sprintf qq(
%s
), $dump; } sub csrftoken { my($this) = @_; if ($this->{csrf}) { return sprintf qq(), $_csrftoken; } else { return qq(); } } sub getcsrf { my($this) = @_; if ($this->{csrf}) { return $_csrftoken; } else { return qq(); } } sub csrfcookie { my($this, $token) = @_; if ($this->{csrf}) { $this->{'_csrf_cookie'} = $token; } return 1; } # # INTERNALS HERE # sub _message { my($this, $message, $id) = @_; return sprintf qq(%s), $id, $message; } sub _tr_field { my($this, $field) = @_; return $this->_tr( join(q( ), @{$field->{classes}}), $field->{id}, $this->_label( $field->{id} . '_input', $field->{label} ), $this->_input( $field->{id} . '_input', $field->{type}, $field->{field}, $field->{value}, $field->{default} # hashref, arrayref or scalar ) . $this->_message($field->{message}, $field->{id} . '_message') ); } sub _tr { my($this, $class, $id, $label, $input) = @_; return sprintf qq(%s%s\n), $id, $class, $label, $class, $input; } sub _table { my($this, $id, $cdata, $legend) = @_; my $html = sprintf qq(), $id; if ($legend) { $html .= sprintf qq(\n), $legend; } $html .= sprintf qq(%s
%s
\n), $cdata; return $html; } sub _normalize_field { my($this, $field) = @_; if (! exists $field->{label}) { $field->{label} = $field->{field}; $field->{label} =~ s/^(.)/uc($1)/e; } if (exists $this->{markrequired} && $this->{field}->{$field->{field}}->{required}) { if ($this->{markrequired} eq 'asterisk') { $field->{label} = $field->{label} . ' *'; } elsif ($this->{markrequired} eq 'bold') { $field->{label} = $this->_b($field->{label}); } else { $field->{label} = $field->{label} . $this->{markrequired}; } } if (! exists $field->{classes}) { $field->{classes} = [ qw(formfield) ]; } if (! exists $field->{id}) { $field->{id} = 'id_formfield_' . $field->{field}; } if (! exists $field->{message}) { $field->{message} = qq(); } if (exists $this->{invalid}->{$field->{field}}) { if (! exists $field->{message}) { $field->{message} = 'invalid input'; } $field->{error} = $this->{invalid}->{$field->{field}}; } if (exists $this->{missing}->{$field->{field}}) { if (! exists $field->{message}) { $field->{message} = 'missing input'; } $field->{error} = 'missing input'; } if (! exists $this->{raw}->{$field->{field}}) { $field->{value} = qq(); } else { $field->{value} = $this->{raw}->{$field->{field}}; } if (! exists $this->{field}->{$field->{field}}->{type}) { $field->{type} = 'text'; } else { $field->{type} = $this->{field}->{$field->{field}}->{type}; } if (! exists $field->{default}) { $field->{default} = qq(); } return $field; } sub _normalize { my($this) = @_; if (exists $this->{meta}->{fields}) { my @normalized; foreach my $field( @{$this->{meta}->{fields}}) { if (! exists $field->{field}) { carp 'unnamed field, ignoring!'; next; } push @normalized, $this->_normalize_field($field); } $this->{meta}->{fields} = \@normalized; } if (exists $this->{meta}->{fieldsets}) { my @fieldsets; foreach my $fieldset (@{$this->{meta}->{fieldsets}}) { if (! exists $fieldset->{id}) { if (! exists $fieldset->{name}) { $fieldset->{id} = 'id_fieldset_' . $.; } else { $fieldset->{id} = 'id_fieldset_' . $fieldset->{name}; } } if (! exists $fieldset->{classes}) { $fieldset->{classes} = [ qw(formfieldset) ]; } if (! exists $fieldset->{legend}) { $fieldset->{legend} = qq(); } my @normalized; foreach my $field (@{$fieldset->{fields}}) { if (! exists $field->{field}) { carp 'unnamed field, ignoring!'; next; } push @normalized, $this->_normalize_field($field); } $fieldset->{fields} = \@normalized; push @fieldsets, $fieldset; } $this->{meta}->{fieldsets} = \@fieldsets; } return; } sub _fieldset { my($this, $class, $id, $legend, $cdata) = @_; return sprintf qq(
%s\n%s\n
\n), $class, $id, $legend, $cdata; } sub _p_field { my($this, $field) = @_; return $this->_p( join(' ', @{$field->{classes}}), $field->{id}, $this->_label( $field->{id} . '_input', $field->{label} ) . $this->_input( $field->{id} . '_input', $field->{type}, $field->{field}, $field->{value}, $field->{default} # hashref, arrayref or scalar ) . $this->_message($field->{message}, $field->{id} . '_message') ); } sub _p { my ($this, $class, $id, $cdata) = @_; return sprintf qq(

%s

\n), $class, $id, $cdata; } sub _label { my ($this, $id, $name) = @_; return sprintf qq(\n ), $id, $name; } sub _input { my ($this, $id, $type, $name, $value, $default) = @_; my $html; if ($type eq 'text' || $type eq 'password') { if (! $value) { $value = $default; } $html = sprintf qq(\n \n), $type, $id, $name, $value; } elsif ($type eq 'choice') { my $html = sprintf qq(\n \n); } elsif ($type eq 'option') { $html = qq(\n); } elsif ($type eq 'textarea') { $html = sprintf qq(\n), $id, $name, $value; } return $html; } sub _b { my($this, $cdata) = @_; return sprintf qq(%s), $cdata; } sub _gen_csrf_token { my($this) = @_; $this->{sha}->add(rand(10)); $this->{sha}->add(time); my $csrftoken = $this->{sha}->hexdigest(); $this->{sha}->reset(); return $csrftoken; } 1; __END__ =head1 NAME HTML::FormsDj - a web forms module the django way =head1 SYNOPSIS In your L app: use HTML::FormsDj; use Data::FormValidator; # a custom DFV constraint. You may also use one # of the supplied ones of Data::FormValidator sub valid_string { return sub { my $dfv = shift; $dfv->name_this('valid_string'); my $val = $dfv->get_current_constraint_value(); return $val =~ /^[a-zA-Z0-9\-\._ ]{4,}$/; } } # our route, we act on GET and POST requests any '/addbook' => sub { my $form = new HTML::FormsDj( # the form, we maintain 2 form variables, title and author field => { title => { type => 'text', validate => valid_string(), required => 1, }, author => { type => 'text', validate => valid_string(), required => 1, }, }, name => 'registerform' ); if ( request->method() eq "POST" ) { # a POST request, fetch the raw input and pass it to the form my %input = params; # "clean" the data, which means to validate it my %clean = $form->cleandata(%input); if ($form->clean() ) { # validation were successfull, so save the data # you'll have to put your own way of data saving # here of course &savebook($clean{title}, $clean{author}); redirect '/booklist'; } else { # nope, something were invalid, put the user # back to the form. his input will be preserved return template 'addbook', { form => $form }; } } else { # a GET request, so just present the empty form template 'addbook', { form => $form }; } }; In your template (views/addbook.tt):
<% form.as_p %>
That's it. Here's the output:

=head1 DESCRIPTION The B module provides a comfortable way to maintain HTML form input. Its main use is for L but can be used with other perl application servers as well, since it doesn't require L to run at all. B aims to behave as much as B Forms system with the excpetion to do it the perl way and without a B feature. It works as follows: You create a new form and tell it which form variables it has to maintain and how to validate them. In your template you can then put out the generated form. B will put back user input into the form if some of the data were invalid. This way your user doesn't have to re-enter anything. You can tweak the behavior and output as much as possible. You can add your own CSS classes, CSS id's, error messages and so on. =head1 CREATING A FORM To create a form, you have to instanciate an B object. Any parameters have to be passed as a hash (of hashes) to B. The most important parameter is the B hash. Here you tell the form, which form variables it has to maintain for you, of which type they are and how to validate them. my $form = new HTML::FormsDj( field => { variablename => { type => 'text', validate => some_validator_func(), required => 1, }, anothervariable => { # .. and so on } } ); A variable can have the following types: B: onelined text fields B: same as above but for passwords B <% END %> =head2 as_is This is in fact no display method, it rather just returns the normalized B hash and NO HTML code. You can use this to generate the HTML yourself, perhaps if the provided methods here are not sufficient for you or if you have to output something different than HTML (e.g. JSON or XML). The structure returned will look like this (based on our example above with some data filled in by a user): { 'fields' => [ { 'classes' => [ 'formfield' ], 'value' => 'Neal Stephenson', 'default' => '', 'type' => 'text', 'id' => 'id_formfield_author', 'label' => 'Author', 'field' => 'author' }, { 'classes' => [ 'formfield' ], 'value' => 'Anathem', 'default' => '', 'type' => 'text', 'id' => 'id_formfield_title', 'label' => 'Title', 'field' => 'title' } ] }; Or, if it contains validation errors: { 'fields' => [ { 'classes' => [ 'formfield' ], 'value' => '', 'default' => '', 'type' => 'text', 'id' => 'id_formfield_author', 'label' => 'Author', 'field' => 'author', 'message' => 'missing input', 'error' => 'missing input', }, { 'classes' => [ 'formfield' ], 'value' => 'Ana', 'default' => '', 'type' => 'text', 'id' => 'id_formfield_title', 'label' => 'Title', 'field' => 'title', 'message' => 'invalid input', 'error' => 'valid_string', } ] }; =head1 INPUT DATA VALIDATION To validate the user input just fetch the HTTP POST data and pass them to the form. The B way: my %input = params; my %clean = $form->cleandata(%input); B now generates based on your configuration L and calls its B method to let it validate the input data. It returns a plain perl hash containing the B data. This hash maybe incomplete if there were validation errors or required fields were not filled in by the user. Therefore, you'll have to check if validation were successfull: =head2 CHECK VALIDATION STATUS Use the method B to check if validation had errors. It returns a true value if not. Example: if ($form->clean() ) { # save the data and tell the user } else { # put the same form back to the user again # so the user has to retry } =head2 CUSTOM CLEAN METHOD Beside the described validation technique you may also supply your own B method to the form, which may do additional checks, such as if a user exists in a database or the like. You can do this by supplying a closure to the B parameter (not method!) when you instantiate the form. Example: my $form = new HTML::FormsDj( .., clean => sub { my (%clean) = @_; my $user = $db->resultset('User')->find({login => $clean{user}}); if($user) { return (0, 'user exists'); } else { return (1, ''); } }, .. ); In this example we're doing exactly this: we check if a user already exists. The closure will get the B<%clean> hash as a parameter, which contains the clean validated form data. B The closure is expected to return a list with two values: true or false and an error message. =head2 USING Data::FormValidator ATTRIBUTES The underlying validator module L supports a couple of attributes which can be used to change its behavior. You can supply such attributes to the form, which will be handed over to L, eg: my $form = new HTML::FormsDj( .., attributes => { filters => ['trim'] }, .. ); The B parameter is just a hashref. Everything inside will be supplied to B. Refer to its documentation which attributes could be used here. =head2 ADVANCED CONTROL OF Data::FormValidator CONSTRAINTS Usually B generates the B used by the B method. Sometimes you might want to supply your own, for instance if you need multiple validators per variable or ir you want to modify the messages which will be returned on errors and the like. You can do this by using the B parameter: my $form = new HTML::FormsDj( .., dfv => {} .. ); Refer to L how to specify/define the dfv profile. In case you've got supplied a dfv profile, the form will not generate its own and just use the one you supplied and it will not check for errors or if it matches the B hash definition. This technique is not recommended for the average user. =head1 ERRORS AND DEBUGGING You can use the form method B, which dumps out the META hash, in your template to see what happens: <% form.dumpmeta %> Beside errors per field there is also a global error variable which can be put out using the B method: <% form.error %> =head1 CROSS SITE REQUEST FORGERY PROTECTION B. B provides CSRF attack protection. Refer to L to learn what it is. To enable CSRF protection, you'll set the B parameter to a true value: my $form = new HTML::FormsDj( .., csrf => 1 .. ); If enabled, the form will generate a unique token for the form based on the field names, some random number and current time. This token must be set as a B during the B request to your form and the very same token has to exist as a B in the form. Since B doesn't depend on L (or any other perl app server), you are responsible for setting and retrieving the cookie. On POST request the value of the cookie must match the value of the hidden variable. If one of them doesn't exist or the two are not the same, B returns B. In addition no B will be returned and no validation will be done. =head2 HOW TO USE CSRF PROTECTION IN A DANCER APP First, enable it using the parameter mentioned above: my $form = new HTML::FormsDj( .., csrf => 1 .. ); In your route for the GET request set the cookie. You can retrieve the actual cookie value by using the B method: cookie csrftoken => $form->getcsrf, expires => "15 minutes"; template 'addbook', { form => $form }; Put this in your code where you're handling the GET request of the form. In your code for the POST request, you'll have to retrieve the cookie and tell the form about it. This has to be done B you call B: if ( request->method() eq "POST" ) { my %input = params; $form->csrfcookie(cookie 'csrftoken'); my %clean = $form->cleandata(%input); if ($form->clean() ) { .. That's it. If you're using B or B you are done and protected from this kind of attacks. If you're creating your html form manually, you'll have to put the hidden value into your template this way: <% form.csrftoken %> =head2 WHY? The forms module might not sound as the right place where to do such things. Maybe a Dancer plugin for this would be the better choice to implement such a feature. However, my idea was, if I am already maintaining forms, why not doing it in a secure way? =head1 TODO =over =item add more unit tests =back =head1 SEE ALSO I recommend you to read the following documents, which are supplied with Perl: perlreftut Perl references short introduction perlref Perl references, the rest of the story perldsc Perl data structures intro perllol Perl data structures: arrays of arrays =head1 LICENSE AND COPYRIGHT Copyright (c) 2012 T. Linden This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 BUGS AND LIMITATIONS See rt.cpan.org for current bugs, if any. =head1 INCOMPATIBILITIES None known. =head1 DIAGNOSTICS To debug HTML::FormsDj use the Perl debugger, see L. =head1 DEPENDENCIES B depends on the module L. It can be used with L, but this is no requirement. =head1 AUTHOR T. Linden =head1 VERSION 0.03 =cut