1 package DevHelp::Validate;
4 use vars qw(@EXPORT_OK @ISA);
5 @EXPORT_OK = qw(dh_validate dh_validate_hash dh_fieldnames dh_configure_fields);
12 match => qr/^[^\@]+\@[\w.-]+\.\w+$/,
13 error => '$n is not a valid email address',
17 match => qr/\d(?:\D*\d){3}/,
18 error => '$n is not a valid phone number',
22 match => qr/\d(?:\D*\d){3}/,
23 error => '$n is not a valid post code',
27 match => qr!^\w+://[\w-]+(?:\.[\w-]+)+(?::\d+)?!,
28 error => '$n is not a valid URL',
32 match => qr!^https?://[\w-]+(?:\.[\w-]+)+(?::\d+)?!,
33 error => '$n is not a valid URL, it must start with http:// or https://',
37 match => qr!^(?:https?://)?[\w-]+(?:\.[\w-]+)+(?::\d+)?!,
38 error => '$n is not a valid URL',
46 newconfirm=>'password',
54 match => qr/\d(?:\D*\d){7}/,
55 error => '$n is not a valid ABN',
59 match => qr/^\D*\d(?:\D*\d){11,15}\D*$/,
60 error => '$n is not a valid credit card number',
66 creditcardexpirysingle =>
72 match => qr/^(\d){3,4}$/,
73 error => '$n is the 3 or 4 digit code on the back of your card',
77 match => qr/^\s*\d{1,6}\s*$/,
78 error => 'Not a valid MIAA membership number',
82 match => qr/^\s*(?:\d+(?:\.\d*)?|\.\d+)\s*$/,
83 error => 'Not a valid number',
87 match => qr/^\s*(?:\d+(?:\.\d\d)?|\.\d\d)\s*$/,
88 error => 'Not a valid money amount',
98 maxdatemsg => 'Your $n must be in the past',
104 maxdatemsg => 'You must be at least 10 years old...',
111 mindatemsg => 'The date entered must be in the future',
117 maxdatemsg => 'The date entered must be in the past',
121 integer => '0-', # 0 or higher
125 integer => '1-', # 1 or higher
129 nomatch => qr/[\x0D\x0A]/,
130 error => '$n may only contain a single line',
134 # we accept 24-hour time, or 12 hour with (a|p|am|pm)
135 match => qr!^(?: # first 24 hour time:
136 (?:[01]?\d|2[0-3]) # hour 0-23
140 (?:0?[1-9]|1[012]) # hour 1-12
141 (?:[:.] # optionally separator followed
142 [0-5]\d)? # by minutes
143 [ap]m? # followed by afternoon/morning
145 error=>'Invalid time $n',
150 my ($class, %opts) = @_;
152 my $self = bless \%opts, $class;
154 # configure validation
155 my $fields = $self->{fields};
156 my $rules = $self->{rules} || {};
159 _get_cfg_fields(\%cfg_rules, $self->{cfg}, $self->{section}, $fields)
160 if $self->{cfg} && $self->{section};
162 for my $rulename (keys %$rules) {
163 unless (exists $cfg_rules{rules}{$rulename}) {
164 $cfg_rules{rules}{$rulename} = $rules->{$rulename};
167 for my $rulename (keys %built_ins) {
168 unless (exists $cfg_rules{rules}{$rulename}) {
169 $cfg_rules{rules}{$rulename} = $built_ins{$rulename};
173 # merge the supplied fields into the config fields
174 my $cfg_fields = $cfg_rules{fields};
175 for my $field ( keys %$fields ) {
176 my $src = $fields->{$field};
178 my $dest = $cfg_fields->{$field} || {};
180 # the config overrides the software supplied fields
181 for my $override (qw(description required required_error)) {
182 if (defined $src->{$override} && !defined $dest->{$override}) {
183 $dest->{$override} = $src->{$override};
187 # but we add rules and required_if
188 if ($dest->{rules}) {
189 my $rules = $src->{rules};
191 # make a copy of the rules array if it's supplied that way so
192 # we don't modify someone else's data
193 $rules = ref $rules ? [ @$rules ] : [ split /;/, $rules ];
195 push @$rules, split /;/, $dest->{rules};
197 elsif ($src->{rules}) {
198 $dest->{rules} = $src->{rules};
200 if ($dest->{required_if}) {
201 $dest->{required_if} .= ";" . $src->{required_if} if $src->{required_if};
203 elsif ($src->{required_if}) {
204 $dest->{required_if} = $src->{required_if};
207 $cfg_fields->{$field} = $dest if keys %$dest;
210 $self->{cfg_fields} = $cfg_fields;
211 $self->{cfg_rules} = $cfg_rules{rules};
217 my ($cgi, $errors, $validation, $cfg, $section) = @_;
219 return DevHelp::Validate::CGI->new(cfg=>$cfg, section=>$section, fields=>$validation->{fields}, rules=>$validation->{rules}, optional=>$validation->{optional})
220 ->validate($cgi, $errors);
223 sub dh_validate_hash {
224 my ($hash, $errors, $validation, $cfg, $section) = @_;
226 return DevHelp::Validate::Hash->new(cfg=>$cfg, section=>$section, fields=>$validation->{fields}, rules=>$validation->{rules}, optional=>$validation->{optional})
227 ->validate($hash, $errors);
231 my ($self, $errors) = @_;
233 my $cfg_fields = $self->{cfg_fields};
234 my $cfg_rules = $self->{cfg_rules};
235 my $optional = $self->{optional};
237 for my $field ( keys %$cfg_fields ) {
238 $self->validate_field($field, $cfg_fields->{$field}, $cfg_rules,
246 my ($self, $field, $info, $rules, $optional, $errors) = @_;
248 my @data = $self->param($field);
250 my $required = $info->{required};
251 if (@data && $data[0] !~ /\S/ && $info->{required_if}) {
252 # field is required if any of the named fields are non-blank
253 for my $testfield (split /;/, $info->{required_if}) {
254 my $testvalue = $self->param($testfield);
255 if (defined $testvalue && $testvalue =~ /\S/) {
262 my $rule_names = $info->{rules};
263 defined $rule_names or $rule_names = '';
264 $rule_names = [ split /;/, $rule_names ] unless ref $rule_names;
266 push @$rule_names, 'required' if $required;
268 @$rule_names or return;
270 RULE: for my $rule_name (@$rule_names) {
271 my $rule = $rules->{$rule_name};
273 $rule = $self->_get_cfg_rule($rule_name);
275 $rules->{$rule_name} = $rule;
278 print STDERR "** Unknown validation rule $rule_name for $field\n";
281 if (!$optional && $rule->{required} && !@data ) {
282 $errors->{$field} = _make_error($field, $info, $rule,
283 $info->{required_error} ||
284 $rule->{required_error} ||
285 '$n is a required field');
288 for my $data (@data) {
289 if ($rule->{required} && $data !~ /\S/) {
290 $errors->{$field} = _make_error($field, $info, $rule,
291 $info->{required_error} ||
292 $rule->{required_error} ||
293 '$n is a required field');
296 if ($rule->{newconfirm}) {
297 my $other = $self->param($rule->{newconfirm});
298 if ($other ne '' || $data ne '') {
299 if ($other ne $data) {
300 $errors->{$field} = _make_error($field, $info, $rule,
301 q!$n doesn't match the password!);
306 if ($data !~ /\S/ && !$rule->{required}) {
309 if ($rule->{match}) {
310 my $match = $rule->{match};
311 unless ($data =~ /$match/) {
312 $errors->{$field} = _make_error($field, $info, $rule);
316 if (defined $rule->{nomatch}) {
317 my $match = $rule->{nomatch};
318 if ($data =~ /$match/) {
319 $errors->{$field} = _make_error($field, $info, $rule);
323 if ($rule->{integer}) {
324 unless ($data =~ /^\s*([-+]?\d+)s*$/) {
325 $errors->{$field} = _make_error($field, $info, $rule,
326 '$n must be a whole number');
330 if (my ($from, $to) = $rule->{integer} =~ /^([+-]?\d+)-([+-]?\d+)$/) {
331 unless ($from <= $num and $num <= $to) {
332 $errors->{$field} = _make_error($field, $info, $rule,
333 $info->{range_error} ||
334 $rule->{range_error} ||
335 "\$n must be in the range $from to $to");
339 elsif (my ($from2) = $rule->{integer} =~ /^([+-]?\d+)-$/) {
340 unless ($from2 <= $num) {
341 $errors->{$field} = _make_error($field, $info, $rule,
342 $info->{range_error} ||
343 $rule->{range_error} ||
344 "\$n must be $from2 or higher");
350 unless ($data =~ m!^\s*(\d+)[-+/](\d+)[-+/](\d+)\s*$!) {
351 $errors->{$field} = _make_error($field, $info, $rule,
352 '$n must be a valid date');
355 my ($day, $month, $year) = ($1, $2, $3);
356 if ($day < 1 || $day > 31) {
357 $errors->{$field} = _make_error($field, $info, $rule,
358 '$n must be a valid date - day out of range');
361 if ($month < 1 || $month > 12) {
362 $errors->{$field} = _make_error($field, $info, $rule,
363 '$n must be a valid date - month out of range');
366 require DevHelp::Date;
368 unless (($year, $month, $day) = DevHelp::Date::dh_parse_date($data, \$msg)) {
369 $errors->{$field} = $msg;
372 if ($rule->{mindate} || $rule->{maxdate}) {
373 my $workdate = sprintf("%04d-%02d-%02d", $year, $month, $day);
374 if ($rule->{mindate}) {
375 my $mindate = DevHelp::Date::dh_parse_date_sql($rule->{mindate});
376 if ($workdate le $mindate) {
378 _make_error($field, $info, $rule,
379 $info->{mindatemsg} || $rule->{mindatemsg} || '$n is too early');
382 if (!$errors->{$field} && $rule->{maxdate}) {
383 my $maxdate = DevHelp::Date::dh_parse_date_sql($rule->{maxdate});
384 if ($workdate ge $maxdate) {
386 _make_error($field, $info, $rule,
387 $info->{mindatemsg} || $rule->{maxdatemsg} || '$n is too late');
392 if ($rule->{confirm}) {
393 my $other = $self->param($rule->{confirm});
394 unless ($other eq $data) {
395 $errors->{$field} = _make_error($field, $info, $rule,
396 q!$n doesn't match the password!);
400 if ($rule->{ccexpiry}) {
401 (my $year_field = $field) =~ s/month/year/;
403 unless ($data =~ /^\s*\d+\s*$/) {
404 $errors->{$field} = _make_error($field, $info, $rule,
405 q!$n month isn't a number!);
408 my $year = $self->param($year_field);
409 unless (defined $year && $year =~ /\s*\d+\s*$/) {
410 $errors->{$field} = _make_error($field, $info, $rule,
411 q!$n year isn't a number!);
414 my ($now_year, $now_month) = (localtime)[5, 4];
417 if ($year < $now_year || $year == $now_year && $data < $now_month) {
418 $errors->{$field} = _make_error($field, $info, $rule,
419 q!$n is in the past, your card has expired!);
423 if ($rule->{ccexpirysingle}) {
424 unless ($data =~ m!^\s*(\d+)\s*/\s*(\d+)+\s*$!) {
425 $errors->{$field} = _make_error($field, $info, $rule,
426 q!$n must be in MM/YY format!);
429 my ($month, $year) = ($1, $2);
431 if ($month < 1 || $month > 12) {
432 $errors->{$field} = _make_error($field, $info, $rule,
433 q!$n month must be between 1 and 12!);
436 my ($now_year, $now_month) = (localtime)[5, 4];
439 if ($year < $now_year || $year == $now_year && $month < $now_month) {
440 $errors->{$field} = _make_error($field, $info, $rule,
441 q!$n is in the past, your card has expired!);
450 my ($field, $info, $rule, $message) = @_;
452 $message ||= $rule->{error} || 'Validation error on field $n';
454 my $name = $info->{description} || $field;
455 $message =~ s/\$n/$name/g;
460 sub _get_cfg_fields {
461 my ($rules, $cfg, $section, $field_hash) = @_;
463 $rules->{rules} = {};
464 $rules->{fields} = {};
466 my $cfg_fields = $rules->{fields};
468 my $fields = $cfg->entry($section, 'fields', '');
469 my @names = ( split(/,/, $fields), keys %$field_hash );
471 for my $field (@names) {
472 for my $cfg_name (qw(required rules description required_error range_error mindatemsg maxdatemsg)) {
473 my $value = $cfg->entry($section, "${field}_$cfg_name");
474 if (defined $value) {
475 $cfg_fields->{$field}{$cfg_name} = $value;
479 my $values = $cfg->entry($section, "${field}_values");
480 if (defined $values) {
482 if ($values =~ /;/) {
483 for my $entry (split /;/, $values) {
484 if ($entry =~ /^([^=]*)=(.*)$/) {
485 push @values, [ $1, $2 ];
488 push @values, [ $entry, $entry ];
494 if ($values =~ s/:([^:]*)$//) {
497 my %entries = $cfg->entriesCS($values);
498 my @order = $cfg->orderCS($values);
501 # we only want the last value in the order
502 @order = reverse grep !$seen{$_}++, reverse @order;
503 @values = map [ $_, $entries{$_} ], @order;
505 $_->[0] =~ s/^\Q$strip// for @values;
508 $cfg_fields->{$field}{values} = \@values;
513 sub dh_configure_fields {
514 my ($fields, $cfg, $section) = @_;
517 _get_cfg_fields(\%cfg_rules, $cfg, $section, $fields);
519 # **FIXME** duplicated code
520 my $cfg_fields = $cfg_rules{fields};
521 for my $field ( keys %$fields ) {
522 my $src = $fields->{$field};
524 my $dest = $cfg_fields->{$field} || {};
526 # the config overrides the software supplied fields
527 for my $override (qw(description required required_error range_error mindatemsg maxdatemsg htmltype type width height size maxlength)) {
528 if (defined $src->{$override} && !defined $dest->{$override}) {
529 $dest->{$override} = $src->{$override};
534 if ($dest->{rules}) {
535 my $rules = $src->{rules} || '';
537 # make a copy of the rules array if it's supplied that way so
538 # we don't modify someone else's data
539 $rules = ref $rules ? [ @$rules ] : [ split /;/, $rules ];
541 push @$rules, split /;/, $dest->{rules};
543 elsif ($src->{rules}) {
544 $dest->{rules} = $src->{rules};
547 $cfg_fields->{$field} = $dest if keys %$dest;
554 my ($self, $rulename) = @_;
556 my %rule = $self->{cfg}->entries("Validation Rule $rulename");
558 keys %rule or return;
564 my ($cfg, $section, $fields) = @_;
566 # this needs to be obsoleted now that dh_validate() checks the config
568 for my $field (keys %$fields) {
569 my $desc = $cfg->entry($section, $field);
570 defined $desc and $fields->{$field}{description} = $desc;
574 package DevHelp::Validate::CGI;
576 @ISA = qw(DevHelp::Validate);
579 my ($self, $field) = @_;
581 $self->{cgi}->param($field);
585 my ($self, $cgi, $errors) = @_;
589 return $self->_validate($errors);
592 package DevHelp::Validate::Hash;
594 @ISA = qw(DevHelp::Validate);
597 my ($self, $field) = @_;
599 my $value = $self->{hash}{$field};
601 defined $value or return;
603 if (ref $value eq 'ARRAY') {
611 my ($self, $hash, $errors) = @_;
613 $self->{hash} = $hash;
615 return $self->_validate($errors);
624 DevHelp::Validate - handy configurable validation, I hope
628 use DevHelp::Validate qw(dh_validate);
630 dh_validate($cgi, \%errors, \%rules, $cfg)
631 or display_errors(..., \%errors);
635 =head1 RULES PARAMETER
637 The rules parameter is a hash with 2 keys:
643 A hash of field names, for each of which the value is a hash.
645 Each hash can have the following keys:
651 A simple rule name, a ';' separated list of rule names or an array
656 A short description of the field, for use in error messages.
662 A hash of rules. See the rules description under L<CONFIGURED
667 =head1 CONFIGURED VALIDATION
669 Rules can be configured in the database.
671 For the specified section name, each key is a CGI field name.
673 The values of those keys gives the name of a validation rule, a string
674 id for internationlization of the field description and a default
675 field description, separated by commas.
677 Each validation rule name has a corresponding section, [Validate Rule
678 I<rule-name>], which describes the rule. Rule names can also refer to
681 Values in the validation rule section are:
687 If this is non-zero the field is required.
691 If present, this is used as a regular expression the field must match.
695 If present, this is used as a regular expression the field must not
700 Message returned as the error if the field fails validation.
704 If set to 1, simply ensures the value is an integer.
706 If set to a range I<integer>-I<integer> then ensures the value is an
707 integer in that range.
711 If set to 1, simply validates the value as a date.
715 =head1 BUILT-IN RULES
727 Matches any valid url, including mailto:, ftp:, etc. No checking of
732 Matches any valid http or https url.
736 Matches web URLs with or without "http://" or "https://".
740 Treats the given field as a confirmation field for a password field
745 Treats the given field as a confirmation field for an optional
746 password field "password".
750 The field is required. This should be used where the logic of the
751 code requires a field, since it cannot be overridden by the config
756 Valid Australian Business Number
758 =item creditcardnumber
760 Valid credit card number (no checksum checks are performed).
762 =item creditcardexpiry
764 Treats the field as the month part of a credit card expiry date, with
765 the year field being the field with the same name, but "month"
766 replaced with "year". The date of expiry is required to be in the
771 Valid MIAA membership number.
775 Valid simple decimal number
779 Valid decimal number with 0 or 2 digits after the decimal point.
783 A valid date. Currently dates are limited to Australian format
784 dd/mm/yy or dd/mm/yyyy format dates.
788 A valid date in the past.
792 A valid date at least 10 years in the past, at most 100 years in the
797 A valid date in the future.
801 An integer greater or equal to zero.
809 The field may not contain line breaks
815 Tony Cook <tony@develop-help.com>