File Coverage

blib/lib/Net/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 Net::Respite::Validate;
2              
3             # Net::Respite::Validate - lighter weight port of CGI::Ex::Validate
4              
5 3     3   228196 use strict;
  3         6  
  3         99  
6 3     3   20 use warnings;
  3         5  
  3         197  
7 3     3   334 use Throw qw(throw);
  3         1619  
  3         15  
8              
9             sub new {
10 254     254 0 109097 my ($class, $args) = @_;
11 254   50     1709 bless $args || {}, $class;
12             }
13              
14             sub validate {
15 254 100   254 0 846 my ($self, $form, $val_hash) = (@_ == 3) ? @_ : (__PACKAGE__->new(), @_);
16 254 50       810 throw "args must be a hashref", {ref => ref($form)} if ref $form ne 'HASH';
17 254         562 my ($fields, $ARGS) = $self->get_ordered_fields($val_hash);
18 254 50       608 return if ! @$fields;
19 254 100 100     588 return if $ARGS->{'validate_if'} && ! $self->check_conditional($form, $ARGS->{'validate_if'});
20              
21 250         584 $self->{'was_checked'} = {};
22 250         566 $self->{'was_valid'} = {};
23 250         459 $self->{'had_error'} = {};
24 250         367 my $found = 1;
25 250         617 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         684 foreach (my $i = 0; $i < @$fields; $i++) {
29 268         411 my $ref = $fields->[$i];
30 268 50 33     607 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         385 $found = 1;
36 268   33     575 my $key = $ref->{'field'} || throw "Missing field key during normal validation";
37              
38             # allow for field names that contain regular expressions
39 268         430 my @keys;
40 268 100       745 if ($key =~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
41 9         58 my ($not, $pat, $opt) = ($1, $3, $4);
42 9         20 $opt =~ tr/g//d;
43 9 50       26 throw "The e option cannot be used on validation keys on field $key" if $opt =~ /e/;
44 9         35 foreach my $_key (sort keys %$form) {
45 15 100 33     188 next if ($not && $_key =~ m/(?$opt:$pat)/) || (! $not && $_key !~ m/(?$opt:$pat)/);
      66        
      66        
46 9         53 push @keys, [$_key, [undef, $1, $2, $3, $4, $5]];
47             }
48             } else {
49 259         600 @keys = ([$key]);
50             }
51              
52 268         480 foreach my $r (@keys) {
53 268         521 my ($field, $ifs_match) = @$r;
54 268 50       820 if (! $checked{$field}++) {
55 268         534 $self->{'was_checked'}->{$field} = 1;
56 268         453 $self->{'was_valid'}->{$field} = 1;
57 268         503 $self->{'had_error'}->{$field} = 0;
58             }
59 268         563 local $ref->{'was_validated'} = 1;
60 268         699 my $err = $self->validate_buddy($form, $field, $ref, $ifs_match);
61 268 100       674 if (!$ref->{'was_validated'}) {
62 17         38 $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       560 if ($err) {
67 114         210 $self->{'was_valid'}->{$field} = 0;
68 114         229 $self->{'had_error'}->{$field} = 0;
69 114 50 66     327 if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
      33        
70 0         0 $hold_error = $err;
71             } else {
72 114 50       237 push @errors, $hold_error ? @$hold_error : @$err;
73 114         563 $hold_error = undef;
74             }
75             } else {
76 154         715 $hold_error = undef;
77             }
78             }
79             }
80 250 50       478 push(@errors, @$hold_error) if $hold_error; # allow for final OR to work
81              
82 250 100 66     948 $self->no_extra_fields($form,$fields,$val_hash,\@errors) if ($ARGS->{'no_extra_fields'} || $self->{'no_extra_fields'});
83              
84 250 100       1115 return if ! @errors; # success
85              
86 120         203 my %uniq;
87             my %error;
88 120         193 foreach my $err (@errors) {
89 121         276 my ($field, $type, $fv, $ifs_match) = @$err;
90 121 50       221 throw "Missing field name", {err => $err} if ! $field;
91 121 50       228 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         270 my $text = $self->get_error_text($err, $ARGS);
96 121 50       831 $error{$field} .= "$text\n" if !$uniq{$field}->{$text}++;
97             }
98 120         453 chomp $error{$_} for keys %error;
99 120 50       270 throw "Validation failed", {errors => \%error} if $ARGS->{'raise_error'};
100 120         956 return \%error;
101             }
102              
103             sub no_extra_fields {
104 26     26 0 42 my ($self,$form,$fields,$fv,$errors,$field_prefix) = @_;
105 26   100     64 $field_prefix ||= '';
106 26   100     69 local $self->{'_recurse'} = ($self->{'_recurse'} || 0) + 1;
107 26 50       41 throw "Max dependency level reached 10" if $self->{'_recurse'} > 10;
108              
109 26         37 my %keys = map { ($_->{'field'} => 1) } @$fields;
  30         76  
110 26         62 foreach my $key (sort keys %$form) {
111 26 100       70 if (ref $form->{$key} eq 'HASH') {
    100          
112 5         7 my $field_type = $fv->{$key}->{'type'};
113 5 100       15 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         6 my $f = [map { {field=>$_} } keys %$field_type];
  4         11  
121 4         21 $self->no_extra_fields($form->{$key},$f,$field_type,$errors,$field_prefix.$key.'.');
122             }
123             } elsif (ref $form->{$key} eq 'ARRAY') {
124 3         6 my $field_type = $fv->{$key}->{'type'};
125 3 50       12 if (!defined $field_type) {
    100          
126             # Do nothing
127             } elsif (ref $field_type eq 'HASH') {
128 2         4 my $f = [map { {field=>$_} } keys %$field_type];
  2         5  
129 2         5 foreach (my $i = 0; $i <= $#{$form->{$key}}; $i ++) {
  8         58  
130 6 50       32 $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       61 next if $keys{$key};
135 7         34 push @$errors, [$field_prefix.$key, 'no_extra_fields', {}, undef];
136             }
137             }
138              
139             sub get_ordered_fields {
140 254     254 0 500 my ($self, $val_hash) = @_;
141 254 50 33     1016 throw "validation must be a hashref" if !$val_hash || ref $val_hash ne 'HASH';
142 254         399 my %ARGS;
143 254 100       934 my @field_keys = grep { /^group\s+(\w+)/ ? do {$ARGS{$1} = $val_hash->{$_}; 0} : 1} sort keys %$val_hash;
  301         1092  
  29         88  
  29         71  
144              
145             # Look first for items in 'group fields' or 'group order'
146 254         454 my $fields;
147 254 100 66     1107 if (my $ref = $ARGS{'fields'} || $ARGS{'order'}) {
148 1 50       7 my $type = $ARGS{'fields'} ? 'group fields' : 'group order';
149 1 50       5 throw "Validation '$type' must be an arrayref when passed" if ref($ref) ne 'ARRAY';
150 1         3 foreach my $field (@$ref) {
151 1 50       4 throw "Non-defined value in '$type'" if ! defined $field;
152 1 50       5 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       4 throw "No element found in '$type' for $field" if ! exists $val_hash->{$field};
160 1 50       5 throw "Found nonhashref value in '$type'" if ref($val_hash->{$field}) ne 'HASH';
161 1         2 my $val = $val_hash->{$field};
162 1 50       5 $val = {%$val, field => $field} if ! $val->{'field'}; # copy the values to add the key
163 1         4 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       3 my %found = map { ref($_) ? ($_->{'field'} => 1) : () } @$fields;
  1         23  
169 1         3 @field_keys = grep { ! $found{$_} } @field_keys;
  2         7  
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         510 foreach my $field (@field_keys) {
175 271 50       696 throw "Found nonhashref value for field $field" if ref($val_hash->{$field}) ne 'HASH';
176 271 50       588 if (defined $val_hash->{$field}->{'field'}) {
177 0         0 push @$fields, $val_hash->{$field};
178             } else {
179 271         536 push @$fields, { %{$val_hash->{$field}}, field => $field };
  271         1297  
180             }
181             }
182              
183 254   50     992 return ($fields || [], \%ARGS);
184             }
185              
186             # allow for optional validation on groups and on individual items
187             sub check_conditional {
188 40     40 0 68 my ($self, $form, $ifs, $ifs_match) = @_;
189 40 50       67 throw "Need reference passed to check_conditional" if ! $ifs;
190 40 50 66     113 $ifs = [$ifs] if ! ref($ifs) || ref($ifs) eq 'HASH';
191              
192 40         78 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         48 my $found = 1;
197 40         89 foreach (my $i = 0; $i <= $#$ifs; $i ++) {
198 40         58 my $ref = $ifs->[$i];
199 40 100       66 if (! ref $ref) {
200 38 50       58 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       129 if ($ref =~ /^function\s*\(/) {
    100          
    100          
206 0         0 next;
207             } elsif ($ref =~ /^(.*?)\s+(was_valid|had_error|was_checked)$/) {
208 11         43 $ref = {field => $1, $2 => 1};
209             } elsif ($ref =~ s/^\s*!\s*//) {
210 2         7 $ref = {field => $ref, max_in_set => "0 of $ref"};
211             } else {
212 25         61 $ref = {field => $ref, required => 1};
213             }
214             }
215             }
216 40 50       70 last if ! $found;
217              
218             # get the field - allow for custom variables based upon a match
219 40   33     419 my $field = $ref->{'field'} || throw "Missing field key during validate_if (possibly used a reference to a main hash *foo -> &foo)";
220 40 50       90 $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  7 100       41  
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     91 $ref->{'max_values'} ||= scalar @{$form->{$field}} if ref $form->{$field} eq 'ARRAY';
  2         5  
224              
225 40         108 my $errs = $self->validate_buddy($form, $field, $ref);
226              
227 40 100       159 $found = 0 if $errs;
228             }
229 40         118 return $found;
230             }
231              
232              
233             # this is where the main checking goes on
234             sub validate_buddy {
235 335     335 0 734 my ($self, $form, $field, $fv, $ifs_match, $field_prefix) = @_;
236 335   100     1239 $field_prefix ||= '';
237 335   100     1163 local $self->{'_recurse'} = ($self->{'_recurse'} || 0) + 1;
238 335 50       797 throw "Max dependency level reached 10" if $self->{'_recurse'} > 10;
239 335         440 my @errors;
240              
241 335 50       617 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       659 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       774 if ($fv->{'canonical'}) {
262 1         3 my $orig = $fv->{'orig_field'} = $field;
263 1         2 $field = $fv->{'canonical'};
264 1         4 $form->{$field} = delete $form->{$orig};
265             }
266              
267 335 100 100     688 if ($fv->{'was_valid'} && ! $self->{'was_valid'}->{$field}) { return [[$field_prefix.$field, 'was_valid', $fv, $ifs_match]]; }
  7         31  
268 328 100 66     1533 if ($fv->{'had_error'} && ! $self->{'had_error'}->{$field}) { return [[$field_prefix.$field, 'had_error', $fv, $ifs_match]]; }
  3         12  
269 325 50 33     697 if ($fv->{'was_checked'} && ! $self->{'was_checked'}->{$field}) { return [[$field_prefix.$field, 'was_checked', $fv, $ifs_match]]; }
  0         0  
270              
271 325 100 100     807 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     676 if (defined($fv->{'default'})
      66        
281             && (!defined($form->{$field}) || (ref($form->{$field}) eq 'ARRAY' ? !@{ $form->{$field} } : !length($form->{$field})))) {
282 5         11 $form->{$field} = $fv->{'default'};
283             }
284              
285 325 100       893 my $values = ref($form->{$field}) eq 'ARRAY' ? $form->{$field} : [$form->{$field}];
286 325         534 my $n_values = @$values;
287              
288             # allow for a few form modifiers
289 325         475 my $modified = 0;
290 325         552 foreach my $value (@$values) {
291 354 100       683 next if ! defined $value;
292 289 50       615 if (! $fv->{'do_not_trim'}) { # whitespace
293 289 50       1676 $modified = 1 if $value =~ s/( ^\s+ | \s+$ )//xg;
294             }
295 289 50       547 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       728 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         425 my %types;
309 325         1209 foreach (sort keys %$fv) {
310 987 100       3165 push @{$types{$1}}, $_ if /^ (compare|custom|equals|match|max_in_set|min_in_set|replace|required_if|sql|type|validate_if) _?\d* $/x;
  229         1022  
311             }
312              
313             # allow for inline specified modifications (ie s/foo/bar/)
314 325 100       736 if ($types{'replace'}) { foreach my $type (@{ $types{'replace'} }) {
  5         5  
  5         10  
315             my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type}
316 5 50       19 : [split(/\s*\|\|\s*/,$fv->{$type})];
317 5         8 foreach my $rx (@$ref) {
318 5 50       28 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         21 my ($pat, $swap, $opt) = ($2, $3, $4);
322 5 50       10 throw "The e option cannot be used in swap on field $field" if $opt =~ /e/;
323 5         16 my $global = $opt =~ s/g//g;
324 5         12 $swap =~ s/\\n/\n/g;
325             my $expand = sub { # code similar to Template::Alloy::VMethod::vmethod_replace
326 9     9   18 my ($text, $start, $end) = @_;
327 9         12 my $copy = $swap;
328 9         30 $copy =~ s{ \\(\\|\$) | \$ (\d+) }{
329 3 50 33     37 $1 ? $1
    50          
330             : ($2 > $#$start || $2 == 0) ? ''
331             : substr($text, $start->[$2], $end->[$2] - $start->[$2]);
332             }exg;
333 9         12 $modified = 1;
334 9         66 $copy;
335 5         73 };
336 5         11 foreach my $value (@$values) {
337 5 50       9 next if ! defined $value;
338 5 100       12 if ($global) { $value =~ s{(?$opt:$pat)}{ $expand->($value, [@-], [@+]) }eg }
  4         237  
  8         45  
339 1         20 else { $value =~ s{(?$opt:$pat)}{ $expand->($value, [@-], [@+]) }e }
  1         22  
340             }
341             }
342             } }
343 325 100 66     701 $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         440 my $needs_val = 0;
347 325         429 my $n_vif = 0;
348 325 100       733 if ($types{'validate_if'}) { foreach my $type (@{ $types{'validate_if'} }) {
  30         32  
  30         48  
349 30         37 $n_vif++;
350 30         40 my $ifs = $fv->{$type};
351 30         57 my $ret = $self->check_conditional($form, $ifs, $ifs_match);
352 30 100       63 $needs_val++ if $ret;
353             } }
354 325 100 100     1087 if (! $needs_val && $n_vif) {
355 17         22 delete $fv->{'was_validated'};
356 17         48 return 0;
357             }
358              
359             # check for simple existence
360             # optionally check only if another condition is met
361 308 100       620 my $is_required = $fv->{'required'} ? 'required' : '';
362 308 100       550 if (! $is_required) {
363 249 100       588 if ($types{'required_if'}) { foreach my $type (@{ $types{'required_if'} }) {
  2         4  
  2         6  
364 2         4 my $ifs = $fv->{$type};
365 2 100       8 next if ! $self->check_conditional($form, $ifs, $ifs_match);
366 1         3 $is_required = $type;
367 1         4 last;
368             } }
369             }
370 308 100 66     960 if ($is_required
      100        
371             && ($n_values == 0 || ($n_values == 1 && (! defined($values->[0]) || ! length $values->[0])))) {
372 24 100       72 return [] if $self->{'_check_conditional'};
373 13         71 return [[$field_prefix.$field, $is_required, $fv, $ifs_match]];
374             }
375              
376 284 100 50     571 my $n = exists($fv->{'min_values'}) ? $fv->{'min_values'} || 0 : 0;
377 284 100       590 if ($n_values < $n) {
378 3 50       38 return [] if $self->{'_check_conditional'};
379 3         24 return [[$field_prefix.$field, 'min_values', $fv, $ifs_match]];
380             }
381              
382 281 100 50     963 $fv->{'max_values'} = $fv->{'min_values'} || 1 if ! exists $fv->{'max_values'};
383 281   50     570 $n = $fv->{'max_values'} || 0;
384 281 100       561 if ($n_values > $n) {
385 4 50       12 return [] if $self->{'_check_conditional'};
386 4         24 return [[$field_prefix.$field, 'max_values', $fv, $ifs_match]];
387             }
388              
389 277         835 foreach ([min => $types{'min_in_set'}],
390             [max => $types{'max_in_set'}]) {
391 551   100     1101 my $keys = $_->[1] || next;
392 14         18 my $minmax = $_->[0];
393 14         20 foreach my $type (@$keys) {
394 14 50       79 $fv->{$type} =~ m/^\s*(\d+)(?i:\s*of)?\s+(.+)\s*$/
395             || throw "Invalid ${minmax}_in_set check $fv->{$type}";
396 14         65 my $n = $1;
397 14         67 foreach my $_field (split /[\s,]+/, $2) {
398 38 100       85 my $ref = ref($form->{$_field}) eq 'ARRAY' ? $form->{$_field} : [$form->{$_field}];
399 38         77 foreach my $_value (@$ref) {
400 42 100 100     147 $n -- if defined($_value) && length($_value);
401             }
402             }
403 14 100 100     68 if ( ($minmax eq 'min' && $n > 0)
      100        
      100        
404             || ($minmax eq 'max' && $n < 0)) {
405 6 100       39 return [] if $self->{'_check_conditional'};
406 5         33 return [[$field_prefix.$field, $type, $fv, $ifs_match]];
407             }
408             }
409             }
410              
411             # at this point @errors should still be empty
412 271         586 my $content_checked; # allow later for possible untainting (only happens if content was checked)
413              
414 271         525 OUTER: foreach my $value (@$values) {
415              
416 280 100       618 if (exists $fv->{'enum'}) {
417 10 100       65 my $ref = ref($fv->{'enum'}) ? $fv->{'enum'} : [split(/\s*\|\|\s*/,$fv->{'enum'})];
418 10         38 my $found = 0;
419 10         20 foreach (@$ref) {
420 30 100 100     112 $found = 1 if defined($value) && $_ eq $value;
421             }
422 10 100       24 if (! $found) {
423 3 50       10 return [] if $self->{'_check_conditional'};
424 3         12 push @errors, [$field_prefix.$field, 'enum', $fv, $ifs_match];
425 3         11 next OUTER;
426             }
427 7         45 $content_checked = 1;
428             }
429              
430             # do specific type checks
431 277 100       531 if (exists $fv->{'type'}) {
432 103 100       267 if (! $self->check_type($value, $fv->{'type'}, $field, $form)){
433 45 50       96 return [] if $self->{'_check_conditional'};
434 45         131 push @errors, [$field_prefix.$field, 'type', $fv, $ifs_match];
435 45         100 next OUTER;
436 58 50 66     190 } if (ref($fv->{'type'}) eq 'HASH' && $form->{$field}) {
437             # recursively check these
438 17         19 foreach my $key (keys %{$fv->{'type'}}) {
  17         34  
439 17 100       18 foreach my $subform (@{ref($form->{$field}) eq 'ARRAY' ? $form->{$field} : [$form->{$field}]}) {
  17         38  
440 27         158 my $errs = $self->validate_buddy($subform, $key, $fv->{'type'}->{$key},[],$field_prefix.$field.'.');
441 27 100       62 push @errors, @$errs if $errs;
442             }
443             }
444 17 100       66 return @errors ? \@errors : 0;
445             }
446 41         67 $content_checked = 1;
447             }
448              
449             # field equals another field
450 215 100       502 if ($types{'equals'}) { foreach my $type (@{ $types{'equals'} }) {
  9         15  
  9         22  
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       30 if ($field2 =~ m/^([\"\'])(.*)\1$/) {
455 2   66     13 $success = (defined($value) && $value eq $2);
456             } else {
457 7 50       27 $field2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  2 100       14  
458 7 100 66     32 if (exists($form->{$field2}) && defined($form->{$field2})) {
    100          
459 5   100     23 $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       59 if ($not ? $success : ! $success) {
    100          
465 5 50       17 return [] if $self->{'_check_conditional'};
466 5         17 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
467 5         52 next OUTER;
468             }
469 4         12 $content_checked = 1;
470             } }
471              
472 210 100       446 if (exists $fv->{'min_len'}) {
473 4         9 my $n = $fv->{'min_len'};
474 4 100 100     19 if (! defined($value) || length($value) < $n) {
475 3 50       9 return [] if $self->{'_check_conditional'};
476 3         14 push @errors, [$field_prefix.$field, 'min_len', $fv, $ifs_match];
477             }
478             }
479              
480 210 100       425 if (exists $fv->{'max_len'}) {
481 27         39 my $n = $fv->{'max_len'};
482 27 100 100     91 if (defined($value) && length($value) > $n) {
483 1 50       6 return [] if $self->{'_check_conditional'};
484 1         5 push @errors, [$field_prefix.$field, 'max_len', $fv, $ifs_match];
485             }
486             }
487              
488             # now do match types
489 210 100       445 if ($types{'match'}) { foreach my $type (@{ $types{'match'} }) {
  15         23  
  15         38  
490             my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type}
491             : ref($fv->{$type}) eq 'Regexp' ? [$fv->{$type}]
492 15 100       90 : [split(/\s*\|\|\s*/,$fv->{$type})];
    100          
493 15         35 foreach my $rx (@$ref) {
494 19 100       46 if (ref($rx) eq 'Regexp') {
495 6 100 66     61 if (! defined($value) || $value !~ $rx) {
496 2         9 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
497             }
498             } else {
499 13 50       78 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         62 my ($not, $pat, $opt) = ($1, $3, $4);
503 13         26 $opt =~ tr/g//d;
504 13 50       31 throw "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
505 13 100 100     533 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         31 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
509             }
510             }
511             }
512 15         44 $content_checked = 1;
513             } }
514              
515             # allow for comparison checks
516 210 100       441 if ($types{'compare'}) { foreach my $type (@{ $types{'compare'} }) {
  38         71  
  38         83  
517 38 50       191 my $ref = ref($fv->{$type}) eq 'ARRAY' ? $fv->{$type} : [split(/\s*\|\|\s*/, $fv->{$type})];
518 38         99 foreach my $comp (@$ref) {
519 38 50       119 next if ! $comp;
520 38         72 my $test = 0;
521 38 100       243 if ($comp =~ /^\s*(>|<|[>
    50          
522 19         105 my ($op, $value2, $field2) = ($1, $2, $3);
523 19 100       48 if ($field2) {
524 4 0       12 $field2 =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
  0 50       0  
525 4 100       11 $value2 = exists($form->{$field2}) ? $form->{$field2} * 1 : 0;
526             }
527 19   100     55 my $val = $value || 0;
528 19         37 $val *= 1;
529 19 100       75 if ($op eq '>' ) { $test = ($val > $value2) }
  3 100       10  
    100          
    100          
    100          
    50          
530 3         9 elsif ($op eq '<' ) { $test = ($val < $value2) }
531 2         8 elsif ($op eq '>=') { $test = ($val >= $value2) }
532 6         15 elsif ($op eq '<=') { $test = ($val <= $value2) }
533 2         24 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       47 my $val = defined($value) ? $value : '';
538 19         80 my ($op, $value2, $field2) = ($1, $2, $3);
539 19 100       68 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         52 $value2 =~ s/^([\"\'])(.*)\1$/$2/;
544             }
545 19 100       77 if ($op eq 'gt') { $test = ($val gt $value2) }
  3 100       8  
    100          
    100          
    100          
    50          
546 3         8 elsif ($op eq 'lt') { $test = ($val lt $value2) }
547 2         6 elsif ($op eq 'ge') { $test = ($val ge $value2) }
548 6         17 elsif ($op eq 'le') { $test = ($val le $value2) }
549 2         5 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       103 if (! $test) {
556 20 50       52 return [] if $self->{'_check_conditional'};
557 20         89 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match];
558             }
559             }
560 38         110 $content_checked = 1;
561             } }
562              
563             # server side sql type
564 210 50       439 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       511 if ($types{'custom'}) { foreach my $type (@{ $types{'custom'} }) {
  7         13  
  7         18  
586 7         12 my $check = $fv->{$type};
587 7         13 my $err;
588 7 100       21 if (ref($check) eq 'CODE') {
589 3         6 my $ok;
590 3 100       6 $err = "$@" if ! eval { $ok = $check->($field, $value, $fv, $type, $form); 1 };
  3         13  
  2         18  
591 3 100       21 next if $ok;
592 2 100 66     40 chomp($err) if !ref($@) && defined($err);
593             } else {
594 4 100       14 next if $check;
595             }
596 4 50       13 return [] if $self->{'_check_conditional'};
597 4 100       21 push @errors, [$field_prefix.$field, $type, $fv, $ifs_match, (defined($err) ? $err : ())];
598 4         12 $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     586 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       1243 return @errors ? \@errors : 0;
618             }
619              
620             sub check_type {
621 103     103 0 242 my ($self, $value, $type) = @_;
622 103 100       242 $type = ref($type) eq 'HASH' ? 'hash' : lc $type;
623 103 50       204 return 0 if ! defined $value;
624 103 50       540 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       38 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       8 return 0 if ! $value;
639 3 50       14 return (4 == grep {/^\d+$/ && $_ < 256} split /\./, $value, 4);
  11         69  
640             } elsif ($type eq 'domain') {
641 9 50 33     37 return 0 if ! $value || length($value) > 255;
642 9 100 100     139 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       122 return 0 if $value !~ /^-? (?: 0 | [1-9]\d*) $/x;
655 10 100       42 return 0 if ($value < 0) ? $value < -2**31 : $value > 2**31-1;
    100          
656             } elsif ($type eq 'uint') {
657 17 100       76 return 0 if $value !~ /^ (?: 0 | [1-9]\d*) $/x;
658 6 100       21 return 0 if $value > 2**32-1;
659             } elsif ($type eq 'num') {
660 19 100       129 return 0 if $value !~ /^-? (?: 0 | [1-9]\d* (?:\.\d+)? | 0?\.\d+) $/x;
661             } elsif ($type eq 'unum') {
662 18 100       116 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         150 return 1;
681             }
682              
683             sub get_error_text {
684 121     121 0 230 my ($self, $err, $extra) = @_;
685 121         252 my ($field, $type, $fv, $ifs_match, $custom_err) = @$err;
686 121 100 66     268 return $custom_err if defined($custom_err) && length($custom_err);
687 120 50       707 my $dig = ($type =~ s/(_?\d+)$//) ? $1 : '';
688              
689 120 50       237 $field = $fv->{'delegate_error'} if $fv->{'delegate_error'};
690 120   33     363 my $name = $fv->{'name'} || $field;
691 120 0       224 if ($ifs_match) { s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg for $field, $name }
  7 100       25  
  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       378 foreach my $key ((length($dig) ? "${type}${dig}_error" : ()), "${type}_error", 'error') {
697 240   50     845 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     935 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         40 return "$name is required.";
709             } elsif ($type eq 'min_values') {
710 3         9 my $n = $fv->{"min_values${dig}"};
711 3 50       8 my $values = ($n == 1) ? 'value' : 'values';
712 3         13 return "$name had less than $n $values.";
713             } elsif ($type eq 'max_values') {
714 4         9 my $n = $fv->{"max_values${dig}"};
715 4 100       12 my $values = ($n == 1) ? 'value' : 'values';
716 4         15 return "$name had more than $n $values.";
717             } elsif ($type eq 'enum') {
718 3         11 return "$name is not in the given list.";
719             } elsif ($type eq 'equals') {
720 5         15 my $field2 = $fv->{"equals${dig}"};
721 5   33     21 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       10  
723 5         20 return "$name did not equal $name2.";
724             } elsif ($type eq 'min_len') {
725 3         8 my $n = $fv->{"min_len${dig}"};
726 3 50       10 my $char = ($n == 1) ? 'character' : 'characters';
727 3         13 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       5 my $char = ($n == 1) ? 'character' : 'characters';
731 1         5 return "$name was more than $n $char.";
732             } elsif ($type eq 'max_in_set') {
733 2         5 my $set = $fv->{"max_in_set${dig}"};
734 2         6 return "Too many fields were chosen from the set ($set)";
735             } elsif ($type eq 'min_in_set') {
736 3         7 my $set = $fv->{"min_in_set${dig}"};
737 3         7 return "Not enough fields were chosen from the set ($set)";
738             } elsif ($type eq 'match') {
739 8         29 return "$name contains invalid characters.";
740             } elsif ($type eq 'compare') {
741 20         73 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         12 return "$name did not match custom test.";
746             } elsif ($type eq 'type') {
747 45         135 my $_type = $fv->{"type${dig}"};
748 45 100       80 $_type = 'hash' if ref($_type) eq 'HASH';
749 45         118 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         19 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;