Skip to content

Commit

Permalink
add support for validating file uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Jun 25, 2015
1 parent 7660450 commit 954e97c
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 39 deletions.
4 changes: 3 additions & 1 deletion Changes
@@ -1,5 +1,7 @@

6.13 2015-06-19
6.13 2015-06-26
- Added support for validating file uploads.
- Added file check to Mojolicious::Validator.

6.12 2015-06-18
- Welcome to the Mojolicious core team Dan Book.
Expand Down
2 changes: 1 addition & 1 deletion lib/Mojolicious.pm
Expand Up @@ -516,7 +516,7 @@ L<Mojolicious::Types> object.
my $validator = $app->validator;
$app = $app->validator(Mojolicious::Validator->new);
Validate parameters, defaults to a L<Mojolicious::Validator> object.
Validate values, defaults to a L<Mojolicious::Validator> object.
# Add validation check
$app->validator->add_check(foo => sub {
Expand Down
17 changes: 12 additions & 5 deletions lib/Mojolicious/Controller.pm
Expand Up @@ -341,6 +341,7 @@ sub validation {
my $header = $req->headers->header('X-CSRF-Token');
my $hash = $req->params->to_hash;
$hash->{csrf_token} //= $header if $token && $header;
$hash->{$_} = $req->every_upload($_) for map { $_->name } @{$req->uploads};
my $validation = $self->app->validator->validation->input($hash);
return $stash->{'mojo.validation'} = $validation->csrf_token($token);
}
Expand Down Expand Up @@ -916,16 +917,22 @@ to inherit query parameters from the current request.
my $validation = $c->validation;
Get L<Mojolicious::Validator::Validation> object for current request to
validate C<GET> and C<POST> parameters extracted from the query string and
C<application/x-www-form-urlencoded> or C<multipart/form-data> message body.
Parts of the request body need to be loaded into memory to parse C<POST>
parameters, so you have to make sure it is not excessively large, there's a
16MB limit by default.
validate file uploads as well as C<GET> and C<POST> parameters extracted from
the query string and C<application/x-www-form-urlencoded> or
C<multipart/form-data> message body. Parts of the request body need to be loaded
into memory to parse C<POST> parameters, so you have to make sure it is not
excessively large, there's a 16MB limit by default.
# Validate GET/POST parameter
my $validation = $c->validation;
$validation->required('title')->size(3, 50);
my $title = $validation->param('title');
# Validate file upload
my $validation = $c->validation;
$validation->required('tarball')->file->size(1, 1024 * 1024);
my $tarball = $validation->param('tarball');
=head2 write
$c = $c->write;
Expand Down
18 changes: 8 additions & 10 deletions lib/Mojolicious/Plugin/TagHelpers.pm
Expand Up @@ -16,17 +16,16 @@ sub register {

my @helpers = (
qw(csrf_field form_for hidden_field javascript label_for link_to),
qw(password_field select_field stylesheet submit_button tag_with_error),
qw(text_area)
qw(select_field stylesheet submit_button tag_with_error text_area)
);
$app->helper($_ => __PACKAGE__->can("_$_")) for @helpers;

$app->helper(check_box =>
sub { _input(shift, shift, value => shift, @_, type => 'checkbox') });
$app->helper(file_field =>
sub { shift; _tag('input', name => shift, @_, type => 'file') });
$app->helper(file_field => sub { _empty_field('file', @_) });
$app->helper(image => sub { _tag('img', src => shift->url_for(shift), @_) });
$app->helper(input_tag => sub { _input(@_) });
$app->helper(password_field => sub { _empty_field('password', @_) });
$app->helper(radio_button =>
sub { _input(shift, shift, value => shift, @_, type => 'radio') });

Expand All @@ -39,6 +38,11 @@ sub _csrf_field {
return _hidden_field($c, csrf_token => $c->helpers->csrf_token, @_);
}

sub _empty_field {
my ($type, $c, $name) = (shift, shift, shift);
return _validation($c, $name, 'input', name => $name, @_, type => $type);
}

sub _form_for {
my ($c, @url) = (shift, shift);
push @url, shift if ref $_[0] eq 'HASH';
Expand Down Expand Up @@ -119,12 +123,6 @@ sub _option {
return _tag('option', %attrs, @$pair[2 .. $#$pair], $pair->[0]);
}

sub _password_field {
my ($c, $name) = (shift, shift);
return _validation($c, $name, 'input', name => $name, @_,
type => 'password');
}

sub _select_field {
my ($c, $name, $options, %attrs) = (shift, shift, shift, @_);

Expand Down
43 changes: 28 additions & 15 deletions lib/Mojolicious/Validator.pm
Expand Up @@ -4,7 +4,13 @@ use Mojo::Base -base;
use Mojolicious::Validator::Validation;

has checks => sub {
{equal_to => \&_equal_to, in => \&_in, like => \&_like, size => \&_size};
{
equal_to => \&_equal_to,
file => sub { !ref $_[2] || !$_[2]->isa('Mojo::Upload') },
in => \&_in,
like => sub { $_[2] !~ $_[3] },
size => \&_size
};
};

sub add_check { $_[0]->checks->{$_[1]} = $_[2] and return $_[0] }
Expand All @@ -25,11 +31,9 @@ sub _in {
return 1;
}

sub _like { $_[2] !~ $_[3] }

sub _size {
my ($validation, $name, $value, $min, $max) = @_;
my $len = length $value;
my $len = ref $value ? $value->size : length $value;
return $len < $min || $len > $max;
}

Expand All @@ -39,7 +43,7 @@ sub _size {
=head1 NAME
Mojolicious::Validator - Validate parameter
Mojolicious::Validator - Validate values
=head1 SYNOPSIS
Expand All @@ -53,33 +57,42 @@ Mojolicious::Validator - Validate parameter
=head1 DESCRIPTION
L<Mojolicious::Validator> validates parameters for L<Mojolicious>.
L<Mojolicious::Validator> validates values for L<Mojolicious>.
=head1 CHECKS
These validation checks are available by default.
=head2 equal_to
$validation->equal_to('foo');
$validation = $validation->equal_to('foo');
Value needs to be equal to the value of another field. Note that this check does
not work with file uploads for security reasons.
=head2 file
$validation = $validation->file;
Value needs to be equal to the value of another field.
Value needs to be a L<Mojo::Upload> object, representing a file upload.
=head2 in
$validation->in(qw(foo bar baz));
$validation = $validation->in(qw(foo bar baz));
Value needs to match one of the values in the list.
Value needs to match one of the values in the list. Note that this check does
not work with file uploads for security reasons.
=head2 like
$validation->like(qr/^[A-Z]/);
$validation = $validation->like(qr/^[A-Z]/);
Value needs to match the regular expression.
Value needs to match the regular expression. Note that this check does not work
with file uploads for security reasons.
=head2 size
$validation->size(2, 5);
$validation = $validation->size(2, 5);
Value length in characters needs to be between these two values.
Expand All @@ -92,8 +105,8 @@ L<Mojolicious::Validator> implements the following attributes.
my $checks = $validator->checks;
$validator = $validator->checks({size => sub {...}});
Registered validation checks, by default only L</"equal_to">, L</"in">,
L</"like"> and L</"size"> are already defined.
Registered validation checks, by default only L</"equal_to">, L</"file">,
L</"in">, L</"like"> and L</"size"> are already defined.
=head1 METHODS
Expand Down
13 changes: 6 additions & 7 deletions lib/Mojolicious/Validator/Validation.pm
Expand Up @@ -191,9 +191,9 @@ array reference.
my $names = $validation->failed;
Return a list of all names for parameters that failed validation.
Return a list of all names for values that failed validation.
# Names of all parameters that failed
# Names of all values that failed
say for @{$validation->failed};
=head2 has_data
Expand Down Expand Up @@ -227,17 +227,16 @@ Change validation L</"topic">.
my $value = $validation->param('foo');
Access validated parameters. If there are multiple values sharing the same
name, and you want to access more than just the last one, you can use
L</"every_param">.
Access validated values. If there are multiple values sharing the same name, and
you want to access more than just the last one, you can use L</"every_param">.
=head2 passed
my $names = $validation->passed;
Return a list of all names for parameters that passed validation.
Return a list of all names for values that passed validation.
# Names of all parameters that passed
# Names of all values that passed
say for @{$validation->passed};
=head2 required
Expand Down
68 changes: 68 additions & 0 deletions t/mojolicious/validation_lite_app.t
Expand Up @@ -3,6 +3,7 @@ use Mojo::Base -strict;
BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' }

use Test::More;
use Mojo::Upload;
use Mojolicious::Lite;
use Test::Mojo;

Expand All @@ -21,6 +22,13 @@ any '/' => sub {
$validation->optional('yada')->two;
} => 'index';

any '/upload' => sub {
my $c = shift;
my $validation = $c->validation;
return $c->render unless $validation->has_data;
$validation->required('foo')->file;
};

any '/forgery' => sub {
my $c = shift;
my $validation = $c->validation;
Expand Down Expand Up @@ -73,6 +81,23 @@ ok $validation->has_error, 'has error';
is_deeply $validation->error('yada'), [qw(equal_to 1 foo)], 'right error';
is_deeply $validation->failed, [qw(baz yada)], 'right names';

# File
$validation = $t->app->validation->input(
{
foo => Mojo::Upload->new,
bar => [Mojo::Upload->new, Mojo::Upload->new],
baz => [Mojo::Upload->new, 'test']
}
);
ok $validation->required('foo')->file->is_valid, 'valid';
ok $validation->required('bar')->file->is_valid, 'valid';
ok $validation->required('baz')->is_valid, 'valid';
ok !$validation->has_error, 'no error';
ok !$validation->file->is_valid, 'not valid';
ok $validation->has_error, 'has error';
is_deeply $validation->error('baz'), [qw(file 1)], 'right error';
is_deeply $validation->failed, ['baz'], 'right names';

# In
$validation = $t->app->validation->input(
{foo => [qw(bar whatever)], baz => [qw(yada ohoh)]});
Expand Down Expand Up @@ -115,6 +140,20 @@ is_deeply $validation->output, {foo => 'bar'}, 'right result';
ok $validation->has_error, 'has error';
is_deeply $validation->error('yada'), [qw(size 1 5 10)], 'right error';

# File size
$validation = $t->app->validation->input(
{
foo => [Mojo::Upload->new->tap(sub { $_->asset->add_chunk('valid') })],
bar => [Mojo::Upload->new->tap(sub { $_->asset->add_chunk('not valid') })]
}
);
ok $validation->required('foo')->file->size(1, 6)->is_valid, 'valid';
ok !$validation->has_error, 'no error';
ok !$validation->required('bar')->file->size(1, 6)->is_valid, 'not valid';
ok $validation->has_error, 'has error';
is_deeply $validation->error('bar'), [qw(size 1 1 6)], 'right error';
is_deeply $validation->failed, ['bar'], 'right names';

# Multiple empty values
$validation = $t->app->validation;
ok !$validation->has_data, 'no data';
Expand Down Expand Up @@ -195,6 +234,29 @@ $t->post_ok('/' => form => {foo => 'no'})->status_is(200)
->element_count_is('.field-with-error', 2)
->element_count_is('.field-with-error', 2, 'with description');

# Successful file upload
$t->post_ok(
'/upload' => form => {foo => {content => 'bar', filename => 'test.txt'}})
->element_exists_not('.field-with-error');

# Successful file upload (multiple files)
$t->post_ok(
'/upload' => form => {
foo => [
{content => 'One', filename => 'one.txt'},
{content => 'Two', filename => 'two.txt'}
]
}
)->element_exists_not('.field-with-error');

# Failed file upload
$t->post_ok('/upload' => form => {foo => 'bar'})
->element_exists('.field-with-error');

# Failed file upload (multiple files)
$t->post_ok('/upload' => form => {foo => ['one', 'two']})
->element_exists('.field-with-error');

# Missing CSRF token
$t->get_ok('/forgery' => form => {foo => 'bar'})->status_is(200)
->content_like(qr/Wrong or missing CSRF token!/)
Expand Down Expand Up @@ -268,6 +330,12 @@ __DATA__
%= password_field 'yada'
% end
@@ upload.html.ep
%= form_for upload => begin
%= file_field 'foo'
%= submit_button
% end
@@ forgery.html.ep
%= form_for forgery => begin
%= 'Wrong or missing CSRF token!' if validation->has_error('csrf_token')
Expand Down

0 comments on commit 954e97c

Please sign in to comment.