File Coverage

blib/lib/Respite/Validate.pm
Criterion Covered Total %
statement 431 529 81.4
branch 365 560 65.1
condition 122 203 60.1
subroutine 12 12 100.0
pod 0 8 0.0
total 930 1312 70.8


line stmt bran cond sub pod time code
1             package Respite::Validate;
2              
3             # Respite::Validate - lighter weight port of CGI::Ex::Validate
4              
5 3     3   372587 use strict;
  3         5  
  3         109  
6 3     3   12 use warnings;
  3         4  
  3         195  
7 3     3   347 use Throw qw(throw);
  3         1697  
  3         17  
8              
9             sub new {
10 254     254 0 118550 my ($class, $args) = @_;
11 254   50     2034 bless $args || {}, $class;
12             }
13              
14             sub validate {
15 254 100   254 0 980 my ($self, $form, $val_hash) = (@_ == 3) ? @_ : (__PACKAGE__->new(), @_);
16 254 50       807 throw "args must be a hashref", {ref => ref($form)} if ref $form ne 'HASH';
17 254         654 my ($fields, $ARGS) = $self->get_ordered_fields($val_hash);
18 254 50       600 return if ! @$fields;
19 254 100 100     624 return if $ARGS->{'validate_if'} && ! $self->check_conditional($form, $ARGS->{'validate_if'});
20              
21 250         652 $self->{'was_checked'} = {};
22 250         526 $self->{'was_valid'} = {};
23 250         429 $self->{'had_error'} = {};
24 250         380 my $found = 1;
25 250         589 my @errors;
26             my $hold_error; # hold the error for a moment - to allow for an "OR" operation
27 250         0 my %checked;
28 250         666 foreach (my $i = 0; $i < @$fields; $i++) {
29 268         451 my $ref = $fields->[$i];
30 268 50 33     712 if (! ref($ref) && $ref eq 'OR') {
31 0 0       0 $i++ if $found; # if found skip the OR altogether
32 0         0 $found = 1; # reset
33 0         0 next;
34             }
35 268         429 $found = 1;
36 268   33     695 my $key = $ref->{'field'} || throw "Missing field key during normal validation";
37              
38             # allow for field names that contain regular expressions
39 268         383 my @keys;
40 268 100       797 if ($key =~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
41 9         53 my ($not, $pat, $opt) = ($1, $3, $4);
42 9         16 $opt =~ tr/g//d;
43 9 50       23 throw "The e option cannot be used on validation keys on field $key" if $opt =~ /e/;
44 9         27 foreach my $_key (sort keys %$form) {
45 15 100 33     168 next if ($not && $_key =~ m/(?$opt:$pat)/) || (! $not && $_key !~ m/(?$opt:$pat)/);
      66        
      66        
46 9         50 push @keys, [$_key, [undef, $1, $2, $3, $4, $5]];
47             }
48             } else {
49 259         690 @keys = ([$key]);
50             }
51              
52 268         598 foreach my $r (@keys) {
53 268         535 my ($field, $ifs_match) = @$r;
54 268 50       890 if (! $checked{$field}++) {
55 268         697 $self->{'was_checked'}->{$field} = 1;
56 268         467 $self->{'was_valid'}->{$field} = 1;
57 268         529 $self->{'had_error'}->{$field} = 0;
58             }
59 268         671 local $ref->{'was_validated'} = 1;
60 268         772 my $err = $self->validate_buddy($form, $field, $ref, $ifs_match);
61 268 100       724 if (!$ref->{'was_validated'}) {
62 17         34 $self->{'was_valid'}->{$field} = 0;
63             }
64              
65             # test the error - if errors occur allow for OR - if OR fails use errors from first fail
66 268 100       530 if ($err) {
67 114         232 $self->{'was_valid'}->{$field} = 0;
68 114         199 $self->{'had_error'}->{$field} = 0;
69 114 50 66     351 if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
      33        
70 0         0 $hold_error = $err;
71             } else {
72 114 50       281 push @errors, $hold_error ? @$hold_error : @$err;
73 114         685 $hold_error = undef;
74             }
75             } else {
76 154         894 $hold_error = undef;
77             }
78             }
79             }
80 250 50       513 push(@errors, @$hold_error) if $hold_error; # allow for final OR to work
81              
82 250 100 66     1250 $self->no_extra_fields($form,$fields,$val_hash,\@errors) if ($ARGS->{'no_extra_fields'} || $self->{'no_extra_fields'});
83              
84 250 100       1430 return if ! @errors; # success
85              
86 120         205 my %uniq;
87             my %error;
88 120         201 foreach my $err (@errors) {
89 121         372 my ($field, $type, $fv, $ifs_match) = @$err;
90 121 50       281 throw "Missing field name", {err => $err} if ! $field;
91 121 50       252 if ($fv->{'delegate_error'}) {
92 0         0 $field = $fv->{'delegate_error'};
93 0 0       0 $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  0 0       0  
94             }
95 121         289 my $text = $self->get_error_text($err, $ARGS);
96 121 50       1019 $error{$field} .= "$text\n" if !$uniq{$field}->{$text}++;
97             }
98 120         447 chomp $error{$_} for keys %error;
99 120 50       285 throw "Validation failed", {errors => \%error} if $ARGS->{'raise_error'};
100 120         1130 return \%error;
101             }
102              
103             sub no_extra_fields {
104 26     26 0 81 my ($self,$form,$fields,$fv,$errors,$field_prefix) = @_;
105 26   100     98 $field_prefix ||= '';
106 26   100     123 local $self->{'_recurse'} = ($self->{'_recurse'} || 0) + 1;
107 26 50       67 throw "Max dependency level reached 10" if $self->{'_recurse'} > 10;
108              
109 26         56 my %keys = map { ($_->{'field'} => 1) } @$fields;
  30         103  
110 26         102 foreach my $key (sort keys %$form) {
111 26 100       102 if (ref $form->{$key} eq 'HASH') {
    100          
112 5         11 my $field_type = $fv->{$key}->{'type'};
113 5 100       18 if(!defined $field_type) {
    50          
114             # Do nothing
115             }
116             elsif (ref $field_type ne 'HASH') {
117 0         0 push @$errors, [$field_prefix.$key, 'no_extra_fields', {}, undef];
118 0         0 next;
119             } else {
120 4         12 my $f = [map { {field=>$_} } keys %$field_type];
  4         15  
121 4         38 $self->no_extra_fields($form->{$key},$f,$field_type,$errors,$field_prefix.$key.'.');
122             }
123             } elsif (ref $form->{$key} eq 'ARRAY') {
124 3         8 my $field_type = $fv->{$key}->{'type'};
125 3 50       15 if (!defined $field_type) {
    100          
126             # Do nothing
127             } elsif (ref $field_type eq 'HASH') {
128 2         5 my $f = [map { {field=>$_} } keys %$field_type];
  2         9  
129 2         6 foreach (my $i = 0; $i <= $#{$form->{$key}}; $i ++) {
  8         28  
130 6 50       55 $self->no_extra_fields($form->{$key}->[$i],$f,$field_type,$errors,$field_prefix.$key.':'.$i.'.') if ref $form->{$key}->[$i];
131             }
132             }
133             }
134 26 100       90 next if $keys{$key};
135 7         42 push @$errors, [$field_prefix.$key, 'no_extra_fields', {}, undef];
136             }
137             }
138              
139             sub get_ordered_fields {
140 254     254 0 577 my ($self, $val_hash) = @_;
141 254 50 33     1143 throw "validation must be a hashref" if !$val_hash || ref $val_hash ne 'HASH';
142 254         446 my %ARGS;
143 254 100       1072 my @field_keys = grep { /^group\s+(\w+)/ ? do {$ARGS{$1} = $val_hash->{$_}; 0} : 1} sort keys %$val_hash;
  301         1276  
  29         117  
  29         80  
144              
145             # Look first for items in 'group fields' or 'group order'
146 254         453 my $fields;
147 254 100 66     1198 if (my $ref = $ARGS{'fields'} || $ARGS{'order'}) {
148 1 50       4 my $type = $ARGS{'fields'} ? 'group fields' : 'group order';
149 1 50       3 throw "Validation '$type' must be an arrayref when passed" if ref($ref) ne 'ARRAY';
150 1         2 foreach my $field (@$ref) {
151 1 50       2 throw "Non-defined value in '$type'" if ! defined $field;
152 1 50       3 if (ref $field) {
    50          
153 0 0       0 throw "Found nonhashref value in '$type'" if ref($field) ne 'HASH';
154 0 0       0 throw "Element missing \"field\" key/value in '$type'" if ! defined $field->{'field'};
155 0         0 push @$fields, $field;
156             } elsif ($field eq 'OR') {
157 0         0 push @$fields, 'OR';
158             } else {
159 1 50       2 throw "No element found in '$type' for $field" if ! exists $val_hash->{$field};
160 1 50       3 throw "Found nonhashref value in '$type'" if ref($val_hash->{$field}) ne 'HASH';
161 1         1 my $val = $val_hash->{$field};
162 1 50       3 $val = {%$val, field => $field} if ! $val->{'field'}; # copy the values to add the key
163 1         2 push @$fields, $val;
164             }
165             }
166              
167             # limit the keys that need to be searched to those not in fields or order
168 1 50       1 my %found = map { ref($_) ? ($_->{'field'} => 1) : () } @$fields;
  1         4  
169 1         2 @field_keys = grep { ! $found{$_} } @field_keys;
  2         4  
170             }
171              
172             # add any remaining field_vals from our original hash
173             # this is necessary for items that weren't in group fields or group order
174 254         618 foreach my $field (@field_keys) {
175 271 50       827 throw "Found nonhashref value for field $field" if ref($val_hash->{$field}) ne 'HASH';
176 271 50       699 if (defined $val_hash->{$field}->{'field'}) {
177 0         0 push @$fields, $val_hash->{$field};
178             } else {
179 271         483 push @$fields, { %{$val_hash->{$field}}, field => $field };
  271         1515  
180             }
181             }
182              
183 254   50     1095 return ($fields || [], \%ARGS);
184             }
185              
186             # allow for optional validation on groups and on individual items
187             sub check_conditional {
188 40     40 0 96 my ($self, $form, $ifs, $ifs_match) = @_;
189 40 50       76 throw "Need reference passed to check_conditional" if ! $ifs;
190 40 50 66     146 $ifs = [$ifs] if ! ref($ifs) || ref($ifs) eq 'HASH';
191              
192 40         94 local $self->{'_check_conditional'} = 1;
193              
194             # run the if options here
195             # multiple items can be passed - all are required unless OR is used to separate
196 40         62 my $found = 1;
197 40         107 foreach (my $i = 0; $i <= $#$ifs; $i ++) {
198 40         76 my $ref = $ifs->[$i];
199 40 100       83 if (! ref $ref) {
200 38 50       75 if ($ref eq 'OR') {
201 0 0       0 $i++ if $found; # if found skip the OR altogether
202 0         0 $found = 1; # reset
203 0         0 next;
204             } else {
205 38 50       174 if ($ref =~ /^function\s*\(/) {
    100          
    100          
206 0         0 next;
207             } elsif ($ref =~ /^(.*?)\s+(was_valid|had_error|was_checked)$/) {
208 11         56 $ref = {field => $1, $2 => 1};
209             } elsif ($ref =~ s/^\s*!\s*//) {
210 2         12 $ref = {field => $ref, max_in_set => "0 of $ref"};
211             } else {
212 25         81 $ref = {field => $ref, required => 1};
213             }
214             }
215             }
216 40 50       103 last if ! $found;
217              
218             # get the field - allow for custom variables based upon a match
219 40   33     101 my $field = $ref->{'field'} || throw "Missing field key during validate_if (possibly used a reference to a main hash *foo -> &foo)";
220 40 50       102 $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  7 100       66  
221              
222             # max_values is properly checked elsewhere, however we need to stub in a value so defaults are properly set
223 40 100 50     110 $ref->{'max_values'} ||= scalar @{$form->{$field}} if ref $form->{$field} eq 'ARRAY';
  2         9  
224              
225 40         132 my $errs = $self->validate_buddy($form, $field, $ref);
226              
227 40 100       314 $found = 0 if $errs;
228             }
229 40         173 return $found;
230             }
231              
232              
233             # this is where the main checking goes on
234             sub validate_buddy {
235 335     335 0 841 my ($self, $form, $field, $fv, $ifs_match, $field_prefix) = @_;
236 335   100     1382 $field_prefix ||= '';
237 335   100     1243 local $self->{'_recurse'} = ($self->{'_recurse'} || 0) + 1;
238 335 50       826 throw "Max dependency level reached 10" if $self->{'_recurse'} > 10;
239 335         482 my @errors;
240              
241 335 50       794 if ($fv->{'exclude_cgi'}) {
242 0         0 delete $fv->{'was_validated'};
243 0         0 return 0;
244             }
245              
246             # allow for field names that contain regular expressions
247 335 50       954 if ($field =~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
248 0         0 my ($not,$pat,$opt) = ($1,$3,$4);
249 0         0 $opt =~ tr/g//d;
250 0 0       0 throw "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
251 0         0 foreach my $_field (sort keys %$form) {
252 0 0 0     0 next if ($not && $_field =~ m/(?$opt:$pat)/) || (! $not && $_field !~ m/(?$opt:$pat)/);
      0        
      0        
253 0         0 my $errs = $self->validate_buddy($form, $_field, $fv, [undef, $1, $2, $3, $4, $5]);
254 0 0       0 push @errors, @$errs if $errs;
255             }
256 0 0       0 return @errors ? \@errors : 0;
257             }
258              
259             # allow for canonical field name (allows api to present one field name but return a different one)
260             # need to do this relatively early since we are changing the value of $field
261 335 100       817 if ($fv->{'canonical'}) {
262 1         5 my $orig = $fv->{'orig_field'} = $field;
263 1         2 $field = $fv->{'canonical'};
264 1         5 $form->{$field} = delete $form->{$orig};
265             }
266              
267 335 100 100     851 if ($fv->{'was_valid'} && ! $self->{'was_valid'}->{$field}) { return [[$field_prefix.$field, 'was_valid', $fv, $ifs_match]]; }
  7         71  
268 328 100 66     791 if ($fv->{'had_error'} && ! $self->{'had_error'}->{$field}) { return [[$field_prefix.$field, 'had_error', $fv, $ifs_match]]; }
  3         18  
269 325 50 33     774 if ($fv->{'was_checked'} && ! $self->{'was_checked'}->{$field}) { return [[$field_prefix.$field, 'was_checked', $fv, $ifs_match]]; }
  0         0  
270              
271 325 100 100     852 if (!exists($form->{$field}) && $fv->{'alias'}) {
272 2 50       5 foreach my $alias (ref($fv->{'alias'}) ? @{$fv->{'alias'}} : $fv->{'alias'}) {
  0         0  
273 2 100       3 next if ! exists $form->{$alias};
274 1         2 $form->{$field} = delete $form->{$alias};
275 1         2 last;
276             }
277             }
278              
279             # allow for default value
280 325 100 66     855 if (defined($fv->{'default'})
      66        
281             && (!defined($form->{$field}) || (ref($form->{$field}) eq 'ARRAY' ? !@{ $form->{$field} } : !length($form->{$field})))) {
282 5         18 $form->{$field} = $fv->{'default'};
283             }
284              
285 325 100       1025 my $values = ref($form->{$field}) eq 'ARRAY' ? $form->{$field} : [$form->{$field}];
286 325         558 my $n_values = @$values;
287              
288             # allow for a few form modifiers
289 325         524 my $modified = 0;
290 325         691 foreach my $value (@$values) {
291 354 100       801 next if ! defined $value;
292 289 50       657 if (! $fv->{'do_not_trim'}) { # whitespace
293 289 50       2066 $modified = 1 if $value =~ s/( ^\s+ | \s+$ )//xg;
294             }
295 289 50       672 if ($fv->{'trim_control_chars'}) {
296 0 0       0 $modified = 1 if $value =~ y/\t/ /;
297 0 0       0 $modified = 1 if $value =~ y/\x00-\x1F//d;
298             }
299 289 50       940 if ($fv->{'to_upper_case'}) { # uppercase
    50          
300 0         0 $value = uc $value;
301 0         0 $modified = 1;
302             } elsif ($fv->{'to_lower_case'}) { # lowercase
303 0         0 $value = lc $value;
304 0         0 $modified = 1;
305             }
306             }
307              
308 325         498 my %types;
309 325         1609 foreach (sort keys %$fv) {
310 987 100       3716 push @{$types{$1}}, $_ if /^ (compare|custom|equals|match|max_in_set|min_in_set|replace|required_if|sql|type|validate_if) _?\d* $/x;
  229         1148  
311             }
312              
313             # allow for inline specified modifications (ie s/foo/bar/)
314 325 100       892 if ($types{'replace'}) { foreach my $type (@{ $types{'replace'} }) {
  5         10  
  5         10  
315             my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type}
316 5 50       26 : [split(/\s*\|\|\s*/,$fv->{$type})];
317 5         11 foreach my $rx (@$ref) {
318 5 50       39 if ($rx !~ m/^\s*s([^\s\w])(.+)\1(.*)\1([eigsmx]*)$/s) {
319 0         0 throw "Not sure how to parse that replace ($rx)";
320             }
321 5         28 my ($pat, $swap, $opt) = ($2, $3, $4);
322 5 50       15 throw "The e option cannot be used in swap on field $field" if $opt =~ /e/;
323 5         17 my $global = $opt =~ s/g//g;
324 5         10 $swap =~ s/\\n/\n/g;
325             my $expand = sub { # code similar to Template::Alloy::VMethod::vmethod_replace
326 9     9   23 my ($text, $start, $end) = @_;
327 9         15 my $copy = $swap;
328 9         22 $copy =~ s{ \\(\\|\$) | \$ (\d+) }{
329 3 50 33     35 $1 ? $1
    50          
330             : ($2 > $#$start || $2 == 0) ? ''
331             : substr($text, $start->[$2], $end->[$2] - $start->[$2]);
332             }exg;
333 9         17 $modified = 1;
334 9         88 $copy;
335 5         32 };
336 5         11 foreach my $value (@$values) {
337 5 50       12 next if ! defined $value;
338 5 100       15 if ($global) { $value =~ s{(?$opt:$pat)}{ $expand->($value, [@-], [@+]) }eg }
  4         341  
  8         51  
339 1         26 else { $value =~ s{(?$opt:$pat)}{ $expand->($value, [@-], [@+]) }e }
  1         7  
340             }
341             }
342             } }
343 325 100 66     840 $form->{$field} = $values->[0] if $modified && $n_values == 1; # put them back into the form if we have modified it
344              
345             # only continue if a validate_if is not present or passes test
346 325         499 my $needs_val = 0;
347 325         473 my $n_vif = 0;
348 325 100       670 if ($types{'validate_if'}) { foreach my $type (@{ $types{'validate_if'} }) {
  30         48  
  30         58  
349 30         45 $n_vif++;
350 30         57 my $ifs = $fv->{$type};
351 30         87 my $ret = $self->check_conditional($form, $ifs, $ifs_match);
352 30 100       82 $needs_val++ if $ret;
353             } }
354 325 100 100     1226 if (! $needs_val && $n_vif) {
355 17         30 delete $fv->{'was_validated'};
356 17         70 return 0;
357             }
358              
359             # check for simple existence
360             # optionally check only if another condition is met
361 308 100       797 my $is_required = $fv->{'required'} ? 'required' : '';
362 308 100       627 if (! $is_required) {
363 249 100       625 if ($types{'required_if'}) { foreach my $type (@{ $types{'required_if'} }) {
  2         4  
  2         3  
364 2         3 my $ifs = $fv->{$type};
365 2 100       6 next if ! $self->check_conditional($form, $ifs, $ifs_match);
366 1         2 $is_required = $type;
367 1         2 last;
368             } }
369             }
370 308 100 66     998 if ($is_required
      100        
371             && ($n_values == 0 || ($n_values == 1 && (! defined($values->[0]) || ! length $values->[0])))) {
372 24 100       90 return [] if $self->{'_check_conditional'};
373 13         87 return [[$field_prefix.$field, $is_required, $fv, $ifs_match]];
374             }
375              
376 284 100 50     638 my $n = exists($fv->{'min_values'}) ? $fv->{'min_values'} || 0 : 0;
377 284 100       596 if ($n_values < $n) {
378 3 50       6 return [] if $self->{'_check_conditional'};
379 3         13 return [[$field_prefix.$field, 'min_values', $fv, $ifs_match]];
380             }
381              
382 281 100 50     1129 $fv->{'max_values'} = $fv->{'min_values'} || 1 if ! exists $fv->{'max_values'};
383 281   50     633 $n = $fv->{'max_values'} || 0;
384 281 100       616 if ($n_values > $n) {
385 4 50       8 return [] if $self->{'_check_conditional'};
386 4         26 return [[$field_prefix.$field, 'max_values', $fv, $ifs_match]];
387             }
388              
389 277         956 foreach ([min => $types{'min_in_set'}],
390             [max => $types{'max_in_set'}]) {
391 551   100     1304 my $keys = $_->[1] || next;
392 14         22 my $minmax = $_->[0];
393 14         29 foreach my $type (@$keys) {
394 14 50       111 $fv->{$type} =~ m/^\s*(\d+)(?i:\s*of)?\s+(.+)\s*$/
395             || throw "Invalid ${minmax}_in_set check $fv->{$type}";
396 14         39 my $n = $1;
397 14         91 foreach my $_field (split /[\s,]+/, $2) {
398 38 100       116 my $ref = ref($form->{$_field}) eq 'ARRAY' ? $form->{$_field} : [$form->{$_field}];
399 38         67 foreach my $_value (@$ref) {
400 42 100 100     185 $n -- if defined($_value) && length($_value);
401             }
402             }
403 14 100 100     159 if ( ($minmax eq 'min' && $n > 0)
      100        
      100        
404             || ($minmax eq 'max' && $n < 0)) {
405 6 100       37 return [] if $self->{'_check_conditional'};
406 5         53 return [[$field_prefix.$field, $type, $fv, $ifs_match]];
407             }
408             }
409             }
410              
411             # at this point @errors should still be empty
412 271         720 my $content_checked; # allow later for possible untainting (only happens if content was checked)
413              
414 271         508 OUTER: foreach my $value (@$values) {
415              
416 280 100       663 if (exists $fv->{'enum'}) {
417 10 100       40 my $ref = ref($fv->{'enum'}) ? $fv->{'enum'} : [split(/\s*\|\|\s*/,$fv->{'enum'})];
418 10         13 my $found = 0;
419 10         12 foreach (@$ref) {
420 30 100 100     63 $found = 1 if defined($value) && $_ eq $value;
421             }
422 10 100       14 if (! $found) {
423 3 50       4 return [] if $self->{'_check_conditional'};
424 3         8 push @errors, [$field_prefix.$field, 'enum', $fv, $ifs_match];
425 3         5 next OUTER;
426             }
427 7         11 $content_checked = 1;
428             }
429              
430             # do specific type checks
431 277 100       628 if (exists $fv->{'type'}) {
432 103 100       325 if (! $self->check_type($value, $fv->{'type'}, $field, $form)){
433 45 50       128 return [] if $self->{'_check_conditional'};
434 45         174 push @errors, [$field_prefix.$field, 'type', $fv, $ifs_match];
435 45         139 next OUTER;
436 58 50 66     211 } if (ref($fv->{'type'}) eq 'HASH' && $form->{$field}) {
437             # recursively check these
438 17         24 foreach my $key (keys %{$fv->{'type'}}) {
  17         52  
439 17 100       25 foreach my $subform (@{ref($form->{$field}) eq 'ARRAY' ? $form->{$field} : [$form->{$field}]}) {
  17         62  
440 27         159 my $errs = $self->validate_buddy($subform, $key, $fv->{'type'}->{$key},[],$field_prefix.$field.'.');
441 27 100       149 push @errors, @$errs if $errs;
442             }
443             }
444 17 100       101 return @errors ? \@errors : 0;
445             }
446 41         77 $content_checked = 1;
447             }
448              
449             # field equals another field
450 215 100       437 if ($types{'equals'}) { foreach my $type (@{ $types{'equals'} }) {
  9         17  
  9         23  
451 9         19 my $field2 = $fv->{$type};
452 9 50       24 my $not = ($field2 =~ s/^!\s*//) ? 1 : 0;
453 9         17 my $success = 0;
454 9 100       31 if ($field2 =~ m/^([\"\'])(.*)\1$/) {
455 2   66     18 $success = (defined($value) && $value eq $2);
456             } else {
457 7 50       25 $field2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  2 100       16  
458 7 100 66     39 if (exists($form->{$field2}) && defined($form->{$field2})) {
    100          
459 5   100     24 $success = (defined($value) && $value eq $form->{$field2});
460             } elsif (! defined($value)) {
461 1         2 $success = 1; # occurs if they are both undefined
462             }
463             }
464 9 50       29 if ($not ? $success : ! $success) {
    100          
465 5 50       14 return [] if $self->{'_check_conditional'};
466 5         21 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
467 5         20 next OUTER;
468             }
469 4         12 $content_checked = 1;
470             } }
471              
472 210 100       444 if (exists $fv->{'min_len'}) {
473 4         8 my $n = $fv->{'min_len'};
474 4 100 100     23 if (! defined($value) || length($value) < $n) {
475 3 50       8 return [] if $self->{'_check_conditional'};
476 3         14 push @errors, [$field_prefix.$field, 'min_len', $fv, $ifs_match];
477             }
478             }
479              
480 210 100       463 if (exists $fv->{'max_len'}) {
481 27         68 my $n = $fv->{'max_len'};
482 27 100 100     136 if (defined($value) && length($value) > $n) {
483 1 50       5 return [] if $self->{'_check_conditional'};
484 1         7 push @errors, [$field_prefix.$field, 'max_len', $fv, $ifs_match];
485             }
486             }
487              
488             # now do match types
489 210 100       578 if ($types{'match'}) { foreach my $type (@{ $types{'match'} }) {
  15         24  
  15         36  
490             my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type}
491             : ref($fv->{$type}) eq 'Regexp' ? [$fv->{$type}]
492 15 100       93 : [split(/\s*\|\|\s*/,$fv->{$type})];
    100          
493 15         36 foreach my $rx (@$ref) {
494 19 100       47 if (ref($rx) eq 'Regexp') {
495 6 100 66     66 if (! defined($value) || $value !~ $rx) {
496 2         58 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
497             }
498             } else {
499 13 50       86 if ($rx !~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
500 0         0 throw "Not sure how to parse that match ($rx)";
501             }
502 13         63 my ($not, $pat, $opt) = ($1, $3, $4);
503 13         26 $opt =~ tr/g//d;
504 13 50       54 throw "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
505 13 100 100     527 if ( ( $not && ( defined($value) && $value =~ m/(?$opt:$pat)/))
      100        
      100        
      100        
506             || (! $not && (! defined($value) || $value !~ m/(?$opt:$pat)/)) ) {
507 6 50       22 return [] if $self->{'_check_conditional'};
508 6         30 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
509             }
510             }
511             }
512 15         43 $content_checked = 1;
513             } }
514              
515             # allow for comparison checks
516 210 100       457 if ($types{'compare'}) { foreach my $type (@{ $types{'compare'} }) {
  38         57  
  38         91  
517 38 50       207 my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type} : [split(/\s*\|\|\s*/, $fv->{$type})];
518 38         91 foreach my $comp (@$ref) {
519 38 50       87 next if ! $comp;
520 38         65 my $test = 0;
521 38 100       282 if ($comp =~ /^\s*(>|<|[>
    50          
522 19         95 my ($op, $value2, $field2) = ($1, $2, $3);
523 19 100       45 if ($field2) {
524 4 0       13 $field2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  0 50       0  
525 4 100       13 $value2 = exists($form->{$field2}) ? $form->{$field2} * 1 : 0;
526             }
527 19   100     52 my $val = $value || 0;
528 19         34 $val *= 1;
529 19 100       83 if ($op eq '>' ) { $test = ($val > $value2) }
  3 100       10  
    100          
    100          
    100          
    50          
530 3         10 elsif ($op eq '<' ) { $test = ($val < $value2) }
531 2         8 elsif ($op eq '>=') { $test = ($val >= $value2) }
532 6         16 elsif ($op eq '<=') { $test = ($val <= $value2) }
533 2         8 elsif ($op eq '!=') { $test = ($val != $value2) }
534 3         10 elsif ($op eq '==') { $test = ($val == $value2) }
535              
536             } elsif ($comp =~ /^\s*(eq|ne|gt|ge|lt|le)\s+(field:(.+)|.+?)\s*$/) {
537 19 100       52 my $val = defined($value) ? $value : '';
538 19         88 my ($op, $value2, $field2) = ($1, $2, $3);
539 19 100       111 if ($field2) {
540 4 0       12 $field2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  0 50       0  
541 4 100       12 $value2 = defined($form->{$field2}) ? $form->{$field2} : '';
542             } else {
543 15         60 $value2 =~ s/^([\"\'])(.*)\1$/$2/;
544             }
545 19 100       84 if ($op eq 'gt') { $test = ($val gt $value2) }
  3 100       7  
    100          
    100          
    100          
    50          
546 3         9 elsif ($op eq 'lt') { $test = ($val lt $value2) }
547 2         7 elsif ($op eq 'ge') { $test = ($val ge $value2) }
548 6         17 elsif ($op eq 'le') { $test = ($val le $value2) }
549 2         6 elsif ($op eq 'ne') { $test = ($val ne $value2) }
550 3         8 elsif ($op eq 'eq') { $test = ($val eq $value2) }
551              
552             } else {
553 0         0 throw "Not sure how to compare \"$comp\"";
554             }
555 38 100       98 if (! $test) {
556 20 50       94 return [] if $self->{'_check_conditional'};
557 20         97 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
558             }
559             }
560 38         100 $content_checked = 1;
561             } }
562              
563             # server side sql type
564 210 50       412 if ($types{'sql'}) { foreach my $type (@{ $types{'sql'} }) {
  0         0  
  0         0  
565 0         0 my $db_type = $fv->{"${type}_db_type"};
566 0 0       0 my $dbh = ($db_type) ? $self->{dbhs}->{$db_type} : $self->{dbh};
567 0 0       0 if (! $dbh) {
    0          
568 0 0       0 throw "Missing dbh for $type type on field $field" . ($db_type ? " and db_type $db_type" : "");
569             } elsif (ref($dbh) eq 'CODE') {
570 0   0     0 $dbh = &$dbh($field, $self) || throw "SQL Coderef did not return a dbh";
571             }
572 0         0 my $sql = $fv->{$type};
573 0         0 my @args = ($value) x $sql =~ tr/?//;
574 0         0 my $return = $dbh->selectrow_array($sql, {}, @args); # is this right - copied from O::FORMS
575 0 0       0 $fv->{"${type}_error_if"} = 1 if ! defined $fv->{"${type}_error_if"};
576 0 0 0     0 if ( (! $return && $fv->{"${type}_error_if"})
      0        
      0        
577             || ($return && ! $fv->{"${type}_error_if"}) ) {
578 0 0       0 return [] if $self->{'_check_conditional'};
579 0         0 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
580             }
581 0         0 $content_checked = 1;
582             } }
583              
584             # server side custom type
585 210 100       532 if ($types{'custom'}) { foreach my $type (@{ $types{'custom'} }) {
  7         15  
  7         18  
586 7         13 my $check = $fv->{$type};
587 7         12 my $err;
588 7 100       21 if (ref($check) eq 'CODE') {
589 3         5 my $ok;
590 3 100       7 $err = "$@" if ! eval { $ok = $check->($field, $value, $fv, $type, $form); 1 };
  3         15  
  2         19  
591 3 100       24 next if $ok;
592 2 100 66     12 chomp($err) if !ref($@) && defined($err);
593             } else {
594 4 100       13 next if $check;
595             }
596 4 50       16 return [] if $self->{'_check_conditional'};
597 4 100       21 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match, (defined($err) ? $err : ())];
598 4         13 $content_checked = 1;
599             } }
600              
601             }
602              
603             # allow for the data to be "untainted"
604             # this is only allowable if the user ran some other check for the datatype
605 254 50 33     755 if ($fv->{'untaint'} && $#errors == -1) {
606 0 0       0 if (! $content_checked) {
607 0         0 push @errors, [$field_prefix.$field, 'untaint', $fv, $ifs_match];
608             } else {
609             # generic untainter - assuming the other required content_checks did good validation
610 0 0       0 $_ = /(.*)/ ? $1 : throw "Couldn't match?" foreach @$values;
611 0 0       0 if ($n_values == 1) {
612 0         0 $form->{$field} = $values->[0];
613             }
614             }
615             }
616              
617 254 100       1474 return @errors ? \@errors : 0;
618             }
619              
620             sub check_type {
621 103     103 0 286 my ($self, $value, $type) = @_;
622 103 100       347 $type = ref($type) eq 'HASH' ? 'hash' : lc $type;
623 103 50       224 return 0 if ! defined $value;
624 103 50       649 if ($type eq 'email') {
    100          
    50          
    100          
    100          
    50          
    50          
    100          
    100          
    100          
    50          
    0          
625 0 0       0 return 0 if ! $value;
626 0 0       0 my ($local_p,$dom) = ($value =~ /^(.+)\@(.+?)$/) ? ($1,$2) : return 0;
627 0 0       0 return 0 if length($local_p) > 60;
628 0 0       0 return 0 if length($dom) > 100;
629 0 0 0     0 return 0 if ! $self->check_type($dom,'domain') && ! $self->check_type($dom,'ip');
630 0 0       0 return 0 if ! $self->check_type($local_p,'local_part');
631             } elsif ($type eq 'hash') {
632 18 100       46 return 0 if ref $value ne 'HASH';
633             } elsif ($type eq 'local_part') {
634 0 0       0 return 0 if ! length($value);
635             # ignoring all valid quoted string local parts
636 0 0       0 return 0 if $value =~ m/[^\w.~!\#\$%\^&*\-=+?]/;
637             } elsif ($type eq 'ip') {
638 3 50       9 return 0 if ! $value;
639 3 50       15 return (4 == grep {/^\d+$/ && $_ < 256} split /\./, $value, 4);
  11         73  
640             } elsif ($type eq 'domain') {
641 9 50 33     40 return 0 if ! $value || length($value) > 255;
642 9 100 100     108 return 0 if $value !~ /^(?:[a-z0-9][a-z0-9\-]{0,62} \.)+ [a-z0-9][a-z0-9\-]{0,62}$/ix
643             || $value =~ m/(?:\.\-|\-\.|\.\.)/;
644             } elsif ($type eq 'url') {
645 0 0       0 return 0 if ! $value;
646 0 0       0 $value =~ s|^https?://([^/]+)||i || return 0;
647 0         0 my $dom = $1;
648 0 0 0     0 return 0 if ! $self->check_type($dom,'domain') && ! $self->check_type($dom,'ip');
649 0 0 0     0 return 0 if $value && ! $self->check_type($value,'uri');
650             } elsif ($type eq 'uri') {
651 0 0       0 return 0 if ! $value;
652 0 0       0 return 0 if $value =~ m/\s+/;
653             } elsif ($type eq 'int') {
654 19 100       134 return 0 if $value !~ /^-? (?: 0 | [1-9]\d*) $/x;
655 10 100       48 return 0 if ($value < 0) ? $value < -2**31 : $value > 2**31-1;
    100          
656             } elsif ($type eq 'uint') {
657 17 100       118 return 0 if $value !~ /^ (?: 0 | [1-9]\d*) $/x;
658 6 100       24 return 0 if $value > 2**32-1;
659             } elsif ($type eq 'num') {
660 19 100       125 return 0 if $value !~ /^-? (?: 0 | [1-9]\d* (?:\.\d+)? | 0?\.\d+) $/x;
661             } elsif ($type eq 'unum') {
662 18 100       161 return 0 if $value !~ /^ (?: 0 | [1-9]\d* (?:\.\d+)? | 0?\.\d+) $/x;
663             } elsif ($type eq 'cc') {
664 0 0       0 return 0 if ! $value;
665 0 0       0 return 0 if $value =~ /[^\d\-\ ]/;
666 0         0 $value =~ s/\D//g;
667 0 0 0     0 return 0 if length($value) > 16 || length($value) < 13;
668              
669             # simple mod10 check
670 0         0 my $sum = my $switch = 0;
671 0         0 foreach my $digit (reverse split //, $value) {
672 0 0       0 $switch = 1 if ++$switch > 2;
673 0         0 my $y = $digit * $switch;
674 0 0       0 $y -= 9 if $y > 9;
675 0         0 $sum += $y;
676             }
677 0 0       0 return 0 if $sum % 10;
678             }
679              
680 56         181 return 1;
681             }
682              
683             sub get_error_text {
684 121     121 0 252 my ($self, $err, $extra) = @_;
685 121         264 my ($field, $type, $fv, $ifs_match, $custom_err) = @$err;
686 121 100 66     287 return $custom_err if defined($custom_err) && length($custom_err);
687 120 50       900 my $dig = ($type =~ s/(_?\d+)$//) ? $1 : '';
688              
689 120 50       306 $field = $fv->{'delegate_error'} if $fv->{'delegate_error'};
690 120   33     404 my $name = $fv->{'name'} || $field;
691 120 0       245 if ($ifs_match) { s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg for $field, $name }
  7 100       23  
  0         0  
692              
693             # type can look like "required" or "required2" or "required100023"
694             # allow for fallback from required100023_error through required_error
695             # look in the passed hash or self first
696 120 50       442 foreach my $key ((length($dig) ? "${type}${dig}_error" : ()), "${type}_error", 'error') {
697 240   50     960 my $msg = $fv->{$key} || $extra->{$key} || next;
698 0 0       0 $msg =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  0 0       0  
699 0         0 $msg =~ s/\$field/$field/g;
700 0         0 $msg =~ s/\$name/$name/g;
701 0 0       0 if (my $value = $fv->{"$type$dig"}) {
702 0 0       0 $msg =~ s/\$value/$value/g if ! ref $value;
703             }
704 0         0 return $msg;
705             }
706              
707 120 100 100     1074 if ($type eq 'required' || $type eq 'required_if') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    50          
    50          
708 13         42 return "$name is required.";
709             } elsif ($type eq 'min_values') {
710 3         5 my $n = $fv->{"min_values${dig}"};
711 3 50       5 my $values = ($n == 1) ? 'value' : 'values';
712 3         8 return "$name had less than $n $values.";
713             } elsif ($type eq 'max_values') {
714 4         10 my $n = $fv->{"max_values${dig}"};
715 4 100       8 my $values = ($n == 1) ? 'value' : 'values';
716 4         17 return "$name had more than $n $values.";
717             } elsif ($type eq 'enum') {
718 3         6 return "$name is not in the given list.";
719             } elsif ($type eq 'equals') {
720 5         15 my $field2 = $fv->{"equals${dig}"};
721 5   33     24 my $name2 = $fv->{"equals${dig}_name"} || "the field $field2";
722 5 50       17 $name2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  1 100       9  
723 5         24 return "$name did not equal $name2.";
724             } elsif ($type eq 'min_len') {
725 3         10 my $n = $fv->{"min_len${dig}"};
726 3 50       8 my $char = ($n == 1) ? 'character' : 'characters';
727 3         16 return "$name was less than $n $char.";
728             } elsif ($type eq 'max_len') {
729 1         5 my $n = $fv->{"max_len${dig}"};
730 1 50       4 my $char = ($n == 1) ? 'character' : 'characters';
731 1         7 return "$name was more than $n $char.";
732             } elsif ($type eq 'max_in_set') {
733 2         6 my $set = $fv->{"max_in_set${dig}"};
734 2         9 return "Too many fields were chosen from the set ($set)";
735             } elsif ($type eq 'min_in_set') {
736 3         11 my $set = $fv->{"min_in_set${dig}"};
737 3         12 return "Not enough fields were chosen from the set ($set)";
738             } elsif ($type eq 'match') {
739 8         33 return "$name contains invalid characters.";
740             } elsif ($type eq 'compare') {
741 20         85 return "$name did not fit comparison.";
742             } elsif ($type eq 'sql') {
743 0         0 return "$name did not match sql test.";
744             } elsif ($type eq 'custom') {
745 3         13 return "$name did not match custom test.";
746             } elsif ($type eq 'type') {
747 45         211 my $_type = $fv->{"type${dig}"};
748 45 100       110 $_type = 'hash' if ref($_type) eq 'HASH';
749 45         175 return "$name did not match type $_type.";
750             } elsif ($type eq 'untaint') {
751 0         0 return "$name cannot be untainted without one of the following checks: enum, equals, match, compare, sql, type, custom";
752             } elsif ($type eq 'no_extra_fields') {
753 7         29 return "$name should not be passed to validate.";
754             }
755 0           throw "Missing error on field $field for type $type$dig";
756             }
757              
758             1;