root/lang/perl/MENTA/trunk/extlib/HTML/AutoForm.pm @ 25687

Revision 25687, 8.6 kB (checked in by kazuho, 5 years ago)

update HTML::AutoForm?

Line 
1package HTML::AutoForm;
2
3use strict;
4use warnings;
5use utf8;
6use Scalar::Util;
7
8use Class::Accessor::Lite;
9use HTML::AutoForm::Error;
10use HTML::AutoForm::Field;
11use HTML::AutoForm::Field::AnyText;
12use HTML::AutoForm::Field::InputCheckable;
13use HTML::AutoForm::Field::InputSet;
14use HTML::AutoForm::Field::Checkbox;
15use HTML::AutoForm::Field::Hidden;
16use HTML::AutoForm::Field::Radio;
17use HTML::AutoForm::Field::Option;
18use HTML::AutoForm::Field::Password;
19use HTML::AutoForm::Field::Select;
20use HTML::AutoForm::Field::Text;
21use HTML::AutoForm::Field::Textarea;
22
23our $VERSION;
24our %Defaults;
25our %Lang_Defaults;
26our $DEFAULT_LANG;
27our $CLASS_PREFIX;
28
29BEGIN {
30    $VERSION = '0.01';
31    %Defaults = (
32        action       => undef,
33        csrf_keyname => '__autoform_csrf_key',
34        fields       => undef, # need to be copied
35        secure       => 1,
36        reset_label  => undef,
37    );
38    %Lang_Defaults = (
39        en => {
40            submit_label => 'Submit Form',
41            error_prefix => '',
42        },
43        ja => {
44            submit_label => 'フォームを投稿',
45            error_prefix => '※',
46        },
47    );
48    Class::Accessor::Lite->mk_accessors(
49        keys %Defaults,
50        keys %{$Lang_Defaults{en}},
51    );
52    $DEFAULT_LANG = 'en';
53    $CLASS_PREFIX = 'autoform';
54};
55
56sub new {
57    my $klass = shift;
58    my %args = @_ == 1 ? %{$_[0]} : @_;
59    my $fields = delete $args{fields} || [];
60    for my $n qw(action) {
61        die 'mandatory attribute "' . $n . '" is missing'
62            unless defined $args{$n};
63    }
64    my $self = bless {
65        %{$Lang_Defaults{$DEFAULT_LANG}},
66        %Defaults,
67        %args,
68        fields => [], # filled afterwards
69    }, $klass;
70    die 'fields should be supplied in: tag => attributes style'
71        unless @$fields % 2 == 0;
72    for (my $i = 0; $i < @$fields; $i += 2) {
73        my $name = $fields->[$i];
74        my $opts = $fields->[$i + 1];
75        die 'field type is missing or invalid'
76            unless $opts->{type} =~ /^(text|hidden|password|radio|select|checkbox|textarea)$/;
77        my $field_klass = 'HTML::AutoForm::Field::' . ucfirst $opts->{type};
78        push @{$self->{fields}}, $field_klass->new(
79            %$opts,
80            name => $name,
81        );
82    }
83    $self;
84}
85
86sub field {
87    my ($self, $n) = @_;
88    for my $f (@{$self->{fields}}) {
89        return $f
90            if $f->name eq $n;
91    }
92    return;
93}
94
95# the default renderer
96sub render {
97    my ($self, $query, $csrf_token) = @_;
98   
99    my $do_validate = $query->request_method eq 'POST'
100            || (! $self->secure && %{$query->Vars});
101   
102    my $html = join(
103        '',
104        '<form action="',
105        _escape_html($self->action),
106        '"',
107        ($self->secure ? ' method="POST"' : ''),
108        '>',
109        '<table class="',
110        $CLASS_PREFIX,
111        '_table">',
112        (map {
113            sub {
114                my $field = shift;
115                my @values = $query->param($_->name);
116                if ($field->type eq 'hidden') {
117                    return $_->render(\@values);
118                }
119                my @r = (
120                    '<tr><th>',
121                    _escape_html($field->label),
122                    '</th><td>',
123                    $field->render(\@values),
124                );
125                if ($do_validate) {
126                    print STDERR "validating: ", $field->name, "\n";
127                    if (my $err = $field->validate($query)) {
128                        push(
129                            @r,
130                            '<div class="',
131                            $CLASS_PREFIX,
132                            '_error">',
133                            _escape_html($self->error_prefix . $err->message),
134                            '</div>',
135                        );
136                    }
137                }
138                push @r, '</td></tr>';
139                @r;
140            }->($_)
141        } @{$self->{fields}}),
142        $self->secure ? (
143            '<input type="hidden" name="',
144            _escape_html($self->csrf_keyname),
145            '" value="',
146            # TODO: use a different id
147            _escape_html($csrf_token),
148            '" />',
149        ) : (),
150        $self->submit_label || $self->reset_label ? (
151            '<tr><th></th><td>',
152            $self->submit_label ? (
153                '<input class="',
154                $CLASS_PREFIX,
155                '_field_submit" type="submit" value="',
156                _escape_html($self->submit_label),
157                '" />',
158            ) : (),
159            $self->reset_label ? (
160                '<input class="',
161                $CLASS_PREFIX,
162                '_field_reset" type="reset" value="',
163                _escape_html($self->reset_label),
164                '" />',
165            ) : (),
166            '</td></tr>',
167        ) : (),
168        '</table></form>',
169    );
170    $html;
171}
172
173sub validate {
174    my ($self, $query, $check_csrf_callback) = @_;
175   
176    for my $f (@{$self->{fields}}) {
177        if (my $error = $f->validate($query)) {
178            return;
179        } elsif (my $h = $f->custom) {
180            if (my $error = $h->($f, $query)) {
181                return;
182            }
183        }
184    }
185    if ($self->secure) {
186        my $ok;
187        if (my $csrf_value = $query->param($self->csrf_keyname)) {
188            if ($check_csrf_callback->($csrf_value)) {
189                $ok = 1;
190            }
191        }
192        return
193            unless $ok;
194    }
195    1;
196}
197
198sub _build_element {
199    my ($tag, $base, $extra, $omit, $append) = @_;
200    my %attr = (
201        (map {
202            ($_ => $base->{$_})
203        } grep {
204            ! exists $omit->{$_} && ! /^(allow_multiple|label|required)$/
205        } keys %$base),
206        %$extra,
207    );
208    my $html = join(
209        '',
210        '<' . $tag,
211        (map {
212            ' ' . $_ . '="' . _escape_html($attr{$_}) . '"'
213        } sort grep {
214            defined $attr{$_}
215        } keys %attr),
216        defined $append ? ('>', $append, '</', $tag, '>') : ' />',
217    );
218    $html;
219}
220
221sub _escape_html {
222    my $str = shift;
223    $str =~ s/&/&amp;/g;
224    $str =~ s/>/&gt;/g;
225    $str =~ s/</&lt;/g;
226    $str =~ s/"/&quot;/g;
227    $str =~ s/'/&#39;/g;
228    $str;
229}
230
2311;
232
233__END__
234
235=head1 NAME
236
237HTML::AutoForm - a standalone HTML form validator and renderer
238
239=head1 SYNOPSIS
240
241 # build form object
242 my $form = HTML::AutoForm->new(
243     fields => [
244         username  => {
245             type       => 'text',
246             required   => 1,
247             min_length => 6,
248             max_length => 8,
249             regexp     => qr/^[0-9a-z_]+$/,
250         },
251 ...
252     ],
253 );
254
255 # validate form
256 my $ok = $form->validate(
257     $query,                # any object that support $query->param('name')
258     sub { ... },           # callback to check if csrf token is valid
259 );
260
261 # render form
262 $html .= $form->render(
263     $query,
264     $csrf_token,
265 );
266
267=head1 DESCRIPTION
268
269HTML::AutoForm is a simple form validator and renderer.
270
271=head1 CONSTRUCTOR
272
273The new function takes following arguments.
274
275=head2 action
276
277action attribute of form tag (mandatory)
278
279=head2 secure
280
281whether or not to limit form submission to POST method, and to perform CSRF protection (default: 1)
282
283=head2 fields
284
285an array of fields in name => attr form.  Following attributes are accepted.
286
287type - type of the field (mandatory, accepted types are: text, hidden, password, radio, select, checkbox, textarea)
288
289required - whether or not user selection (or input) to the field is mandatory.  For fields that support multiple selection (like checkbox), set a numeral to require certain number of items to be selected.  Or set an arrayref to specify the range of number of choices.
290
291min_length - minimum length of value (for editable fields)
292
293max_length - maximum length of value (for editable fields)
294
295regexp     - regular expression used for validation (for editable fields)
296
297custom     - custom validation rule (set as subref, for editable fields)
298
299label      - label of the field (default is ucfirst(name))
300
301options    - an arrayref of value => attributes (for checkbox, radio, select types)
302
303Other attributes are treated as ordinal HTML attributes.
304
305=head1 METHODS
306
307=head2 action
308
309action attribute of form tag
310
311=head2 csrf_keyname
312
313set parameter name used for CSRF protection (default: '__autoform_csrf_key')
314
315=head2 field($field_name)
316
317accessor for field object by name
318
319=head2 fields
320
321accessor for field object array
322
323=head2 render($query [, $csrf_token])
324
325default HTML renderer
326
327=head2 secure
328
329whether or not to protect the form againts CSRF attacks
330
331=head2 validate($query [, csrf_token_validator ])
332
333query validator
334
335=head1 AUTHOR
336
337Kazuho Oku E<lt>kazuhooku !@#$%^&* gmail.comE<gt>
338
339=head1 LICENSE
340
341This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
342
343=cut
Note: See TracBrowser for help on using the browser.