formvalidator 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/CHANGELOG +28 -0
- data/README +24 -0
- data/README.rdoc +120 -0
- data/TODO +5 -0
- data/examples/README +10 -0
- data/examples/extend.rb +9 -0
- data/examples/file.rb +24 -0
- data/examples/profiles/extension.rb +12 -0
- data/examples/profiles/my_profile.rb +30 -0
- data/examples/simple.rb +32 -0
- data/examples/standard.rb +52 -0
- data/formvalidator.gemspec +16 -0
- data/formvalidator.rb +888 -0
- data/install.rb +9 -0
- data/tests/regress.rb +518 -0
- data/tests/testprofile.rb +30 -0
- metadata +59 -0
data/AUTHORS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Travis Whitton <whitton@atlantic.net>
|
data/CHANGELOG
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
--------------------------------------------------------------------------------
|
2
|
+
2/13/2003 version 0.1.0
|
3
|
+
|
4
|
+
o Initial release.
|
5
|
+
|
6
|
+
--------------------------------------------------------------------------------
|
7
|
+
2/26/2003 version 0.1.1
|
8
|
+
|
9
|
+
o Fixed validate method return value to reflect documented return value.
|
10
|
+
o Fixed a bug related to applying proc constraints.
|
11
|
+
o Fixed a bug in untainting of proc constraints.
|
12
|
+
o Wrote better unit tests.
|
13
|
+
o Code cleanup.
|
14
|
+
|
15
|
+
--------------------------------------------------------------------------------
|
16
|
+
4/8/2003 version 0.1.2
|
17
|
+
|
18
|
+
o Fixed constraint method to ignore empty fields.
|
19
|
+
o Fixed regexp_constraint method to ignore empty fields.
|
20
|
+
o Wrote additional unit test.
|
21
|
+
|
22
|
+
--------------------------------------------------------------------------------
|
23
|
+
8/14/2003 version 0.1.3
|
24
|
+
|
25
|
+
o Fixed credit card type detection bugs
|
26
|
+
o Fixed a bug regarding invalid field accounting
|
27
|
+
|
28
|
+
--------------------------------------------------------------------------------
|
data/README
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
FormValidator Library
|
3
|
+
|
4
|
+
Feb. 13, 2003 Travis Whitton <whitton@atlantic.net>
|
5
|
+
|
6
|
+
FormValidator is a full featured form validation library written in pure ruby.
|
7
|
+
See README.rdoc for more details.
|
8
|
+
|
9
|
+
[How to install]
|
10
|
+
1. su to root
|
11
|
+
2. ruby install.rb
|
12
|
+
build the docs if you want to
|
13
|
+
3. rdoc --main README.rdoc formvalidator.rb README.rdoc
|
14
|
+
|
15
|
+
[Copying]
|
16
|
+
FormValidator extension library is copywrited free software by Travis Whitton
|
17
|
+
<whitton@atlantic.net>. You can redistribute it under the terms specified in
|
18
|
+
the COPYING file of the Ruby distribution.
|
19
|
+
|
20
|
+
[WARRANTY]
|
21
|
+
THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
22
|
+
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
23
|
+
WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
|
24
|
+
PURPOSE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
== Purpose
|
2
|
+
|
3
|
+
FormValidator is a form validation libary derived from Perl's
|
4
|
+
Data::FormValidator module. When you are coding a web application one of the
|
5
|
+
most tedious though crucial tasks is to validate user's input (usually
|
6
|
+
submitted by way of an HTML form). You have to check that each required field
|
7
|
+
is present and that all fields have valid data. (Does the phone input looks
|
8
|
+
like a phone number? Is that a plausible email address? Is the YY state valid?
|
9
|
+
etc.) For a simple form, this is not really a problem but as forms get more
|
10
|
+
complex and you code more of them this task becames really boring and tedious.
|
11
|
+
|
12
|
+
FormValidator lets you define profiles which declare the required fields and
|
13
|
+
their format. When you are ready to validate the user's input, you tell
|
14
|
+
FormValidator the profile to apply to the users data and you immediately know
|
15
|
+
the valid, missing, invalid, and unknown fields. Instance variables are filled
|
16
|
+
with the results of the validation run so you know which fields failed what
|
17
|
+
tests.
|
18
|
+
|
19
|
+
You are then free to use this information to build a nice display to the user
|
20
|
+
telling which fields had problems.
|
21
|
+
|
22
|
+
== Input Profile Specification
|
23
|
+
|
24
|
+
To create a FormValidator object, do one of the following:
|
25
|
+
# profile data will be fed in from a hash
|
26
|
+
fv = FormValidator.new
|
27
|
+
# profile data will be read from someprofile.rb
|
28
|
+
fv = FormValidator.new("someprofile.rb")
|
29
|
+
|
30
|
+
In the first case, a profile hash and form hash must be specified to the
|
31
|
+
validate method(see below). In the second case, the input profile is loaded
|
32
|
+
from somefile.rb, and a label would be given to the validate method to
|
33
|
+
indicate which profile to apply to the form. If this sounds confusing, see
|
34
|
+
the Usage section below, and you'll get the idea.
|
35
|
+
|
36
|
+
For all allowable profile methods, please see FormValidator::InputProfile.
|
37
|
+
|
38
|
+
== Usage
|
39
|
+
|
40
|
+
The simplest and most common usage is to specify the profile in a hash
|
41
|
+
and pass it along with the form data into the FormValidator::validate method.
|
42
|
+
|
43
|
+
=== Validate a simple form
|
44
|
+
|
45
|
+
require "formvalidator"
|
46
|
+
|
47
|
+
form = {
|
48
|
+
"phone" => "home phone: (123) 456-7890",
|
49
|
+
"zip" => "32608-1234",
|
50
|
+
"rogue" => "some unknown field"
|
51
|
+
}
|
52
|
+
|
53
|
+
profile = {
|
54
|
+
:required => [:name, :zip],
|
55
|
+
:optional => :phone,
|
56
|
+
:filters => :strip,
|
57
|
+
:field_filters => { :phone => :phone },
|
58
|
+
:constraints => {
|
59
|
+
:phone => :american_phone,
|
60
|
+
:zip => [
|
61
|
+
:zip,
|
62
|
+
{
|
63
|
+
:name => "pure_digit",
|
64
|
+
:constraint => /^\d+$/
|
65
|
+
}
|
66
|
+
]
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
fv = FormValidator.new
|
71
|
+
fv.validate(form, profile)
|
72
|
+
fv.valid # <== {"phone"=>" (123) 456-7890"}
|
73
|
+
fv.invalid # <== {"zip"=>["pure_digit"]}
|
74
|
+
fv.missing # <== ["name"]
|
75
|
+
fv.unknown # <== ["rogue"]
|
76
|
+
|
77
|
+
=== Validate from a file
|
78
|
+
|
79
|
+
require "formvalidator"
|
80
|
+
|
81
|
+
form = {
|
82
|
+
"phone" => "home phone: (123) 456-7890",
|
83
|
+
"zip" => "32608-1234",
|
84
|
+
"rogue" => "some unknown field"
|
85
|
+
}
|
86
|
+
|
87
|
+
fv = FormValidator.new("profile_file.rb")
|
88
|
+
fv.validate(form, :testinfo)
|
89
|
+
|
90
|
+
Contents of profile_file.rb
|
91
|
+
{
|
92
|
+
:testinfo =>
|
93
|
+
{
|
94
|
+
:required => [:name, :zip],
|
95
|
+
:optional => [:arr, :phone],
|
96
|
+
:filters => :strip,
|
97
|
+
:field_filters => { :phone => :phone },
|
98
|
+
:constraints => {
|
99
|
+
:phone => :american_phone,
|
100
|
+
:zip => [
|
101
|
+
:zip,
|
102
|
+
{
|
103
|
+
:name => "pure_digit",
|
104
|
+
:constraint => /^\d+$/
|
105
|
+
}
|
106
|
+
]
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
When placing profile data in a separate file, you must tag each profile
|
112
|
+
with a label such as testinfo in the example above. This allows multiple
|
113
|
+
profiles to be stored in a single file for easy access.
|
114
|
+
|
115
|
+
== Credits
|
116
|
+
|
117
|
+
FormValidator is written by Travis Whitton and is based on Perl's
|
118
|
+
Data::FormValidator, which was written by Francis J. Lacoste. The credit
|
119
|
+
card validation portion was adapted from MiniVend, which is written by
|
120
|
+
Bruce Albrecht.
|
data/TODO
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Patches are Welcome!
|
2
|
+
--------------------------------------------------------------------------------
|
3
|
+
o Make constraints handle form elements with multiple fields.
|
4
|
+
o Make field filters accept proc objects.
|
5
|
+
o Make a tutorial on using FormValidator on the Ruby Wiki.
|
data/examples/README
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
This directory contains some example usage of the FormValidator class.
|
2
|
+
|
3
|
+
Files and Directories
|
4
|
+
o extend.rb -> example of how to add a custom filter
|
5
|
+
o file.rb -> demonstrates loading a profile from a file
|
6
|
+
o profiles -> directory of stored profiles
|
7
|
+
o simple.rb -> demonstrates basic functionality
|
8
|
+
o standard.rb -> demonstrates some of the extended features
|
9
|
+
|
10
|
+
The
|
data/examples/extend.rb
ADDED
data/examples/file.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "../formvalidator"
|
2
|
+
|
3
|
+
form = {
|
4
|
+
"first_name" => "Travis",
|
5
|
+
"last_name" => "whitton",
|
6
|
+
"age" => "22",
|
7
|
+
"home_phone" => "home phone: (123) 456-7890",
|
8
|
+
"fax" => "some bogus fax",
|
9
|
+
"street" => "111 NW 1st Street",
|
10
|
+
"city" => "fakeville",
|
11
|
+
"state" => "FL",
|
12
|
+
"zipcode" => "32608-1234",
|
13
|
+
"email" => "whitton@atlantic.net",
|
14
|
+
"password" => "foo123",
|
15
|
+
"paytype" => "Check",
|
16
|
+
"check_no" => "123456789"
|
17
|
+
}
|
18
|
+
|
19
|
+
fv = FormValidator.new("profiles/my_profile.rb")
|
20
|
+
fv.validate(form, :customer)
|
21
|
+
puts "valid -> " + fv.valid.inspect
|
22
|
+
puts "invalid -> " + fv.invalid.inspect
|
23
|
+
puts "missing -> " + fv.missing.inspect
|
24
|
+
puts "unknown -> " + fv.unknown.inspect
|
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
:customer =>
|
3
|
+
{
|
4
|
+
:required_regexp => /name/,
|
5
|
+
:required => [ :home_phone, :age, :password ],
|
6
|
+
:optional => %w{fax email paytype check_no country},
|
7
|
+
:optional_regexp => /street|city|state|zipcode/,
|
8
|
+
:require_some => { :check_or_cc => [1, %w{cc_num check_no}] },
|
9
|
+
:dependencies => { :paytype => { :CC => [ :cc_type, :cc_exp ],
|
10
|
+
:Check => :check_no },
|
11
|
+
:street => [ :city, :state, :zipcode ]
|
12
|
+
},
|
13
|
+
:dependency_groups => { :password_group => [ :password,
|
14
|
+
:password_confirmation ]
|
15
|
+
},
|
16
|
+
:filters => :strip,
|
17
|
+
:field_filters => { :home_phone => :phone,
|
18
|
+
:check_no => :digit,
|
19
|
+
:cc_no => :digit
|
20
|
+
},
|
21
|
+
:field_filter_regexp_map => { /name/ => :capitalize },
|
22
|
+
:constraints => { :age => /^1?\d{1,2}$/,
|
23
|
+
:fax => :american_phone,
|
24
|
+
:state => :state_or_province,
|
25
|
+
:email => :email },
|
26
|
+
:defaults => { :country => "USA" },
|
27
|
+
:constraint_regexp_map => { /code/ => :zip },
|
28
|
+
:untaint_all_constraints => true
|
29
|
+
}
|
30
|
+
}
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "../formvalidator"
|
2
|
+
|
3
|
+
form = {
|
4
|
+
"phone" => "home phone: (123) 456-7890",
|
5
|
+
"zip" => "32608-1234",
|
6
|
+
"rogue" => "some unknown field"
|
7
|
+
}
|
8
|
+
|
9
|
+
profile = {
|
10
|
+
:required => [:name, :zip],
|
11
|
+
:optional => :phone,
|
12
|
+
:filters => :strip,
|
13
|
+
:field_filters => { :phone => :phone },
|
14
|
+
:constraints => {
|
15
|
+
:phone => :american_phone,
|
16
|
+
:zip => [
|
17
|
+
:zip,
|
18
|
+
{
|
19
|
+
:name => "pure_digit",
|
20
|
+
:constraint => /^\d+$/
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
fv = FormValidator.new
|
27
|
+
fv.validate(form, profile)
|
28
|
+
puts fv.valid.inspect # <== {"phone"=>" (123) 456-7890"}
|
29
|
+
puts fv.invalid.inspect # <== {"zip"=>["pure_digit"]}
|
30
|
+
puts fv.missing.inspect # <== ["name"]
|
31
|
+
puts fv.unknown.inspect # <== ["rogue"]
|
32
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "../formvalidator"
|
2
|
+
|
3
|
+
form = {
|
4
|
+
"first_name" => "Travis",
|
5
|
+
"last_name" => "whitton",
|
6
|
+
"age" => "22",
|
7
|
+
"home_phone" => "home phone: (123) 456-7890",
|
8
|
+
"fax" => "some bogus fax",
|
9
|
+
"street" => "111 NW 1st Street",
|
10
|
+
"city" => "fakeville",
|
11
|
+
"state" => "FL",
|
12
|
+
"zipcode" => "32608-1234",
|
13
|
+
"email" => "whitton@atlantic.net",
|
14
|
+
"password" => "foo123",
|
15
|
+
"paytype" => "Check",
|
16
|
+
"check_no" => "123456789"
|
17
|
+
}
|
18
|
+
|
19
|
+
profile = {
|
20
|
+
:required_regexp => /name/,
|
21
|
+
:required => [ :home_phone, :age, :password ],
|
22
|
+
:optional => %w{fax email paytype check_no country},
|
23
|
+
:optional_regexp => /street|city|state|zipcode/,
|
24
|
+
:require_some => { :check_or_cc => [1, %w{cc_num check_no}] },
|
25
|
+
:dependencies => { :paytype => { :CC => [ :cc_type, :cc_exp ],
|
26
|
+
:Check => :check_no },
|
27
|
+
:street => [ :city, :state, :zipcode ]
|
28
|
+
},
|
29
|
+
:dependency_groups => { :password_group => [ :password,
|
30
|
+
:password_confirmation ]
|
31
|
+
},
|
32
|
+
:filters => :strip,
|
33
|
+
:field_filters => { :home_phone => :phone,
|
34
|
+
:check_no => :digit,
|
35
|
+
:cc_no => :digit
|
36
|
+
},
|
37
|
+
:field_filter_regexp_map => { /name/ => :capitalize },
|
38
|
+
:constraints => { :age => /^1?\d{1,2}$/,
|
39
|
+
:fax => :american_phone,
|
40
|
+
:state => :state_or_province,
|
41
|
+
:email => :email },
|
42
|
+
:defaults => { :country => "USA" },
|
43
|
+
:constraint_regexp_map => { /code/ => :zip },
|
44
|
+
:untaint_all_constraints => true
|
45
|
+
}
|
46
|
+
|
47
|
+
fv = FormValidator.new
|
48
|
+
fv.validate(form, profile)
|
49
|
+
puts "valid -> " + fv.valid.keys.sort.inspect
|
50
|
+
puts "invalid -> " + fv.invalid.inspect
|
51
|
+
puts "missing -> " + fv.missing.inspect
|
52
|
+
puts "unknown -> " + fv.unknown.inspect
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{formvalidator}
|
3
|
+
s.version = "0.1.3"
|
4
|
+
s.date = Time.now
|
5
|
+
s.summary = %q{FormValidator is a Ruby port of Perl's Data::FormValidator library.}
|
6
|
+
s.author = %q{Travis Whitton}
|
7
|
+
s.email = %q{whitton@atlantic.net}
|
8
|
+
s.homepage = %q{http://grub.ath.cx/formvalidator/}
|
9
|
+
s.require_path = %q{.}
|
10
|
+
s.autorequire = %q{formvalidator}
|
11
|
+
s.files = Dir.glob('**/*')
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
14
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
15
|
+
s.test_files = %w{tests/regress.rb}
|
16
|
+
end
|
data/formvalidator.rb
ADDED
@@ -0,0 +1,888 @@
|
|
1
|
+
class FormValidator
|
2
|
+
VERSION = "0.1.2"
|
3
|
+
|
4
|
+
# Constructor.
|
5
|
+
def initialize(profile=nil)
|
6
|
+
@profile_file = profile # File to load profile from
|
7
|
+
@profiles = nil # Hash of profiles
|
8
|
+
|
9
|
+
# If profile is a hash, there's no need to load it from a file.
|
10
|
+
if Hash === profile
|
11
|
+
@profiles = @profile_file
|
12
|
+
@profile_file = nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# This method runs all tests specified inside of profile on form.
|
17
|
+
# It sets the valid, invalid, missing and unknown instance variables
|
18
|
+
# to the appropriate values and then returns true if no errors occured
|
19
|
+
# or false otherwise.
|
20
|
+
def validate(form, profile)
|
21
|
+
setup(form, profile)
|
22
|
+
field_filters
|
23
|
+
filters
|
24
|
+
field_filter_regexp_map
|
25
|
+
required
|
26
|
+
required_regexp
|
27
|
+
require_some
|
28
|
+
optional
|
29
|
+
optional_regexp
|
30
|
+
delete_empty
|
31
|
+
delete_unknown
|
32
|
+
dependencies
|
33
|
+
dependency_groups
|
34
|
+
defaults
|
35
|
+
untaint_constraint_fields
|
36
|
+
untaint_all_constraints
|
37
|
+
constraint_regexp_map
|
38
|
+
constraints
|
39
|
+
!(missing.length > 0 || invalid.length > 0 || unknown.length > 0)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a distinct list of missing fields.
|
43
|
+
def missing
|
44
|
+
@missing_fields.uniq.sort
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a distinct list of unknown fields.
|
48
|
+
def unknown
|
49
|
+
(@unknown_fields - @invalid_fields.keys).uniq.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a hash of valid fields and their associated values
|
53
|
+
def valid
|
54
|
+
@form
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a hash of invalid fields and their failed constraints
|
58
|
+
def invalid
|
59
|
+
@invalid_fields
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Load profile with a hash describing valid input.
|
65
|
+
def setup(form_data, profile)
|
66
|
+
@untaint_all = false # Untaint all constraints?
|
67
|
+
@missing_fields = [] # Contains missing fields
|
68
|
+
@unknown_fields = [] # Unknown fields
|
69
|
+
@required_fields = [] # Contains required fields
|
70
|
+
@invalid_fields = {} # Contains invalid fields
|
71
|
+
@untaint_fields = [] # Fields which should be untainted
|
72
|
+
@require_some_fields = [] # Contains require_some fields
|
73
|
+
@optional_fields = [] # Contains optional fields
|
74
|
+
@profile = {} # Contains profile data from wherever it's loaded
|
75
|
+
|
76
|
+
if Hash === profile
|
77
|
+
@profile = profile
|
78
|
+
else
|
79
|
+
load_profiles
|
80
|
+
@profile = @profiles[profile]
|
81
|
+
end
|
82
|
+
check_profile_syntax(@profile)
|
83
|
+
@form = form_data
|
84
|
+
@profile = convert_profile(@profile)
|
85
|
+
end
|
86
|
+
|
87
|
+
# This converts all Symbols in a profile to strings for internal use.
|
88
|
+
# This is the magic behind why a profile can use symbols or strings to
|
89
|
+
# define valid input; therefore, making everybody happy.
|
90
|
+
def convert_profile(profile)
|
91
|
+
profile.each do |key,value|
|
92
|
+
unless Hash === profile[key]
|
93
|
+
# Convert data to an array and turn symbols into strings.
|
94
|
+
profile[key] = strify_array(value)
|
95
|
+
# Turn single items back into single items.
|
96
|
+
profile[key] = profile[key][0] unless Array === value
|
97
|
+
else
|
98
|
+
# Recurse hashes nested to an unlimited level and stringify them
|
99
|
+
profile[key] = strify_hash(profile[key])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# [:a, :b, :c, [:d, :e, [:f, :g]]] -> ["a", "b", "c", ["d", "e", ["f", "g"]]]
|
105
|
+
def strify_array(array)
|
106
|
+
array.to_a.map do |m|
|
107
|
+
m = (Array === m) ? strify_array(m) : m
|
108
|
+
m = (Hash === m) ? strify_hash(m) : m
|
109
|
+
Symbol === m ? m.to_s : m
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Stringifies all keys and elements of a hash.
|
114
|
+
def strify_hash(hash)
|
115
|
+
newhash = {}
|
116
|
+
conv = lambda {|key| Symbol === key ? key.to_s : key}
|
117
|
+
hash.each do |key,value|
|
118
|
+
if Hash === value
|
119
|
+
newhash[conv.call(key)] = strify_hash(value)
|
120
|
+
else
|
121
|
+
newhash[conv.call(key)] = strify_array(value)
|
122
|
+
newhash.delete(key) if Symbol === key
|
123
|
+
unless Array === value
|
124
|
+
newhash[conv.call(key)] = newhash[conv.call(key)][0]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
newhash
|
129
|
+
end
|
130
|
+
|
131
|
+
# Does some checks on the profile file and loads it.
|
132
|
+
def load_profiles
|
133
|
+
file = @profile_file
|
134
|
+
# File must exist.
|
135
|
+
raise "No such file: #{file}" unless test(?f, file)
|
136
|
+
# File must be readable.
|
137
|
+
raise "Can't read #{file}" unless test(?r, file)
|
138
|
+
mtime = File.stat(file).mtime
|
139
|
+
# See if an already loaded profile has been modified.
|
140
|
+
return if @profiles and @profiles_mtime <= mtime
|
141
|
+
# Eval to turn it into a hash.
|
142
|
+
fh = File.open(file)
|
143
|
+
@profiles = eval(fh.read)
|
144
|
+
fh.close
|
145
|
+
# Die if it's not a hash.
|
146
|
+
raise "Input profiles didn't return a Hash" unless Hash === @profiles
|
147
|
+
@profiles_mtime = mtime
|
148
|
+
end
|
149
|
+
|
150
|
+
# Ensure that profile contains valid syntax.
|
151
|
+
def check_profile_syntax(profile)
|
152
|
+
raise "Invalid input profile: Must be a Hash" unless Hash === profile
|
153
|
+
valid_profile_keys =
|
154
|
+
[ :optional, :required, :required_regexp, :require_some,
|
155
|
+
:optional_regexp, :constraints, :constraint_regexp_map,
|
156
|
+
:dependencies, :dependency_groups, :defaults, :filters,
|
157
|
+
:field_filters, :field_filter_regexp_map,
|
158
|
+
:missing_optional_valid, :validator_packages,
|
159
|
+
:untaint_constraint_fields, :untaint_all_constraints ]
|
160
|
+
|
161
|
+
profile.keys.map do |key|
|
162
|
+
unless valid_profile_keys.include?(key)
|
163
|
+
raise "Invalid input profile: #{key} is not a valid profile key"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# This module contains all the valid methods that can be invoked from an
|
169
|
+
# input profile. See the method definitions below for more information.
|
170
|
+
module InputProfile
|
171
|
+
# Takes an array, symbol, or string.
|
172
|
+
#
|
173
|
+
# Any fields in this list which are not present in the user input will be
|
174
|
+
# reported as missing.
|
175
|
+
#
|
176
|
+
# :required => [:name, :age, :phone]
|
177
|
+
def required
|
178
|
+
@profile[:required].to_a.each do |field|
|
179
|
+
@required_fields << field
|
180
|
+
@missing_fields.push(field) if @form[field].to_s.empty?
|
181
|
+
end
|
182
|
+
@missing_fields
|
183
|
+
end
|
184
|
+
|
185
|
+
# Takes an array, symbol, or string.
|
186
|
+
#
|
187
|
+
# Any fields in this list which are present in the form hash will go
|
188
|
+
# through any specified constraint checks and filters. Any fields that
|
189
|
+
# aren't in the optional or required list are reported as unknown and
|
190
|
+
# deleted from the valid hash.
|
191
|
+
#
|
192
|
+
# :optional => [:name, :age, :phone]
|
193
|
+
def optional
|
194
|
+
@profile[:optional].to_a.each do |field|
|
195
|
+
@optional_fields << field unless @optional_fields.include?(field)
|
196
|
+
end
|
197
|
+
@optional_fields
|
198
|
+
end
|
199
|
+
|
200
|
+
# Takes a regular expression.
|
201
|
+
#
|
202
|
+
# Specifies additional fieds which are required. If a given form element
|
203
|
+
# matches the regexp, it must have data, or it will be reported in the
|
204
|
+
# missing field list.
|
205
|
+
#
|
206
|
+
# :required_regexp => /name/
|
207
|
+
def required_regexp
|
208
|
+
@form.keys.each do |elem|
|
209
|
+
@profile[:required_regexp].to_a.each do |regexp|
|
210
|
+
regexp = Regexp.new(regexp)
|
211
|
+
if elem =~ regexp
|
212
|
+
@required_fields << elem unless @required_fields.include?(elem)
|
213
|
+
@missing_fields.push(elem) if @form[elem].to_s.empty?
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
@missing_fields
|
218
|
+
end
|
219
|
+
|
220
|
+
# Takes a regular expression.
|
221
|
+
#
|
222
|
+
# Any form fields that match the regexp specified are added to the list
|
223
|
+
# of optional fields.
|
224
|
+
#
|
225
|
+
# :required_regexp => /name/
|
226
|
+
def optional_regexp
|
227
|
+
@form.keys.each do |elem|
|
228
|
+
@profile[:optional_regexp].to_a.each do |regexp|
|
229
|
+
regexp = Regexp.new(regexp)
|
230
|
+
if elem =~ regexp
|
231
|
+
@optional_fields << elem unless @optional_fields.include?(elem)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
@optional_fields
|
236
|
+
end
|
237
|
+
|
238
|
+
# Takes a hash with each key pointing to an array.
|
239
|
+
#
|
240
|
+
# The first field in the array is the number of fields that must be filled.
|
241
|
+
# The field is an array of fields to choose from. If the required number
|
242
|
+
# of fields are not found, the key name is reported in the list of missing
|
243
|
+
# fields.
|
244
|
+
#
|
245
|
+
# :require_some => { :check_or_cc => [1, %w{cc_num check_no}] }
|
246
|
+
def require_some
|
247
|
+
return nil unless Hash === @profile[:require_some]
|
248
|
+
@profile[:require_some].keys.each do |group|
|
249
|
+
enough = 0
|
250
|
+
num_to_require, fields = @profile[:require_some][group]
|
251
|
+
fields.each do |field|
|
252
|
+
unless @require_some_fields.include?(field)
|
253
|
+
@require_some_fields << field
|
254
|
+
end
|
255
|
+
enough += 1 unless @form[field].to_s.empty?
|
256
|
+
end
|
257
|
+
@missing_fields.push(group.to_s) unless (enough >= num_to_require)
|
258
|
+
end
|
259
|
+
@missing_fields
|
260
|
+
end
|
261
|
+
|
262
|
+
# Takes a hash.
|
263
|
+
#
|
264
|
+
# Fills in defaults but does not override required fields.
|
265
|
+
#
|
266
|
+
# :defaults => { :country => "USA" }
|
267
|
+
def defaults
|
268
|
+
return nil unless Hash === @profile[:defaults]
|
269
|
+
keys_defaulted = []
|
270
|
+
@profile[:defaults].each do |key,value|
|
271
|
+
if @form[key].to_s.empty?
|
272
|
+
@form[key] = value.to_s
|
273
|
+
keys_defaulted.push(key)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
keys_defaulted
|
277
|
+
end
|
278
|
+
|
279
|
+
# Takes a hash.
|
280
|
+
#
|
281
|
+
# This hash which contains dependencies information. This is for the case
|
282
|
+
# where one optional fields has other requirements. The dependencies can be
|
283
|
+
# specified with an array. For example, if you enter your credit card
|
284
|
+
# number, the field cc_exp and cc_type should also be present. If the
|
285
|
+
# dependencies are specified with a hash then the additional constraint is
|
286
|
+
# added that the optional field must equal a key on the form for the
|
287
|
+
# dependencies to be added.
|
288
|
+
#
|
289
|
+
# :dependencies => { :paytype => { :CC => [ :cc_type, :cc_exp ],
|
290
|
+
# :Check => :check_no
|
291
|
+
# }}
|
292
|
+
#
|
293
|
+
# :dependencies => { :street => [ :city, :state, :zipcode ] }
|
294
|
+
def dependencies
|
295
|
+
return nil unless Hash === @profile[:dependencies]
|
296
|
+
@profile[:dependencies].each do |field,deps|
|
297
|
+
if Hash === deps
|
298
|
+
deps.keys.each do |key|
|
299
|
+
if @form[field].to_s == key
|
300
|
+
deps[key].to_a.each do |dep|
|
301
|
+
@missing_fields.push(dep) if @form[dep].to_s.empty?
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
else
|
306
|
+
if not @form[field].to_s.empty?
|
307
|
+
deps.to_a.each do |dep|
|
308
|
+
@missing_fields.push(dep) if @form[dep].to_s.empty?
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
@missing_fields
|
314
|
+
end
|
315
|
+
|
316
|
+
# Takes a hash pointing to an array.
|
317
|
+
#
|
318
|
+
# If no fields are filled, then fine, but if any fields are filled, then
|
319
|
+
# all must be filled.
|
320
|
+
#
|
321
|
+
# :dependency_groups => { :password_group => [ :pass1, :pass2 ] }
|
322
|
+
def dependency_groups
|
323
|
+
return nil unless Hash === @profile[:dependency_groups]
|
324
|
+
require_all = false
|
325
|
+
@profile[:dependency_groups].values.each do |val|
|
326
|
+
require_all = true unless val.select{|group| @form[group]}.empty?
|
327
|
+
end
|
328
|
+
if require_all
|
329
|
+
@profile[:dependency_groups].values.each do |deps|
|
330
|
+
deps.each do |dep|
|
331
|
+
@missing_fields.push(dep) if @form[dep].to_s.empty?
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
@missing_fields
|
336
|
+
end
|
337
|
+
|
338
|
+
# Takes an array, symbol, or string.
|
339
|
+
#
|
340
|
+
# Specified filters will be applied to ALL fields.
|
341
|
+
#
|
342
|
+
# :filters => :strip
|
343
|
+
def filters
|
344
|
+
@profile[:filters].to_a.each do |filter|
|
345
|
+
if respond_to?("filter_#{filter}".intern)
|
346
|
+
@form.keys.each do |field|
|
347
|
+
# If a key has multiple elements, apply filter to each element
|
348
|
+
if @form[field].to_a.length > 1
|
349
|
+
@form[field].each_index do |i|
|
350
|
+
elem = @form[field][i]
|
351
|
+
@form[field][i] = self.send("filter_#{filter}".intern, elem)
|
352
|
+
end
|
353
|
+
else
|
354
|
+
if not @form[field].to_s.empty?
|
355
|
+
@form[field] =
|
356
|
+
self.send("filter_#{filter}".intern, @form[field].to_s)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
@form
|
363
|
+
end
|
364
|
+
|
365
|
+
# Takes a hash.
|
366
|
+
#
|
367
|
+
# Applies one or more filters to the specified field.
|
368
|
+
# See FormValidator::Filters for a list of builtin filters.
|
369
|
+
#
|
370
|
+
# :field_filters => { :home_phone => :phone }
|
371
|
+
def field_filters
|
372
|
+
@profile[:field_filters].to_a.each do |field,filters|
|
373
|
+
filters.to_a.each do |filter|
|
374
|
+
if respond_to?("filter_#{filter}".intern)
|
375
|
+
# If a key has multiple elements, apply filter to each element
|
376
|
+
if @form[field].to_a.length > 1
|
377
|
+
@form[field].each_index do |i|
|
378
|
+
elem = @form[field][i]
|
379
|
+
@form[field][i] = self.send("filter_#{filter}".intern, elem)
|
380
|
+
end
|
381
|
+
else
|
382
|
+
@form[field] =
|
383
|
+
self.send("filter_#{filter}".intern, @form[field].to_s)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
@form
|
389
|
+
end
|
390
|
+
|
391
|
+
# Takes a regexp.
|
392
|
+
#
|
393
|
+
# Applies one or more filters to fields matching regexp.
|
394
|
+
#
|
395
|
+
# :field_filter_regexp_map => { /name/ => :capitalize }
|
396
|
+
def field_filter_regexp_map
|
397
|
+
@profile[:field_filter_regexp_map].to_a.each do |re,filters|
|
398
|
+
filters.to_a.each do |filter|
|
399
|
+
if respond_to?("filter_#{filter}".intern)
|
400
|
+
@form.keys.select {|key| key =~ re}.each do |match|
|
401
|
+
# If a key has multiple elements, apply filter to each element
|
402
|
+
if @form[match].to_a.length > 1
|
403
|
+
@form[match].each_index do |i|
|
404
|
+
elem = @form[match][i]
|
405
|
+
@form[match][i] = self.send("filter_#{filter}".intern, elem)
|
406
|
+
end
|
407
|
+
else
|
408
|
+
@form[match] =
|
409
|
+
self.send("filter_#{filter}".intern, @form[match].to_s)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
@form
|
416
|
+
end
|
417
|
+
|
418
|
+
# Takes true.
|
419
|
+
#
|
420
|
+
# If this is set, all fields which pass a constraint check are assigned
|
421
|
+
# the return value of the constraint check, and their values are untainted.
|
422
|
+
# This is overridden by untaint_constraint_fields.
|
423
|
+
#
|
424
|
+
# :untaint_all_constraints => true
|
425
|
+
def untaint_all_constraints
|
426
|
+
if @profile[:untaint_all_constraints]
|
427
|
+
@untaint_all = true unless @profile[:untaint_constraint_fields]
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Takes an array, symbol, or string.
|
432
|
+
#
|
433
|
+
# Any field found in this array will be assigned the return value
|
434
|
+
# of the constraint check it passes, and it's value will be untainted.
|
435
|
+
#
|
436
|
+
# :untaint_constraint_fields => %w{ name age }
|
437
|
+
def untaint_constraint_fields
|
438
|
+
@profile[:untaint_constraint_fields].to_a.each do |field|
|
439
|
+
@untaint_fields.push(field)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Takes a hash.
|
444
|
+
#
|
445
|
+
# Applies constraints to fields matching regexp and adds failed fields to
|
446
|
+
# the list of invalid fields. If untainting is enabled then the form
|
447
|
+
# element will be set to the result of the constraint method.
|
448
|
+
#
|
449
|
+
# :constraint_regexp_map => { /code/ => :zip }
|
450
|
+
def constraint_regexp_map
|
451
|
+
return nil unless Hash === @profile[:constraint_regexp_map]
|
452
|
+
@profile[:constraint_regexp_map].each do |re,constraint|
|
453
|
+
re = Regexp.new(re)
|
454
|
+
@form.keys.select {|key| key =~ re}.each do |match|
|
455
|
+
unless @form[match].to_s.empty?
|
456
|
+
do_constraint(match, [constraint].flatten)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Takes a hash.
|
463
|
+
#
|
464
|
+
# Apply constraint to each key and add failed fields to the invalid list.
|
465
|
+
# If untainting is enabled then the form element will be set to the result
|
466
|
+
# of the constraint method. Valid constraints can be one of the following:
|
467
|
+
# * Array
|
468
|
+
# Any constraint types listed below can be applied in series.
|
469
|
+
# * Builtin constraint function (See: FormValidator::Constraints)
|
470
|
+
# :fax => :american_phone
|
471
|
+
# * Regular expression
|
472
|
+
# :age => /^1?\d{1,2}$/
|
473
|
+
# * Proc object
|
474
|
+
# :num => proc {|n| ((n % 2).zero?) ? n : nil}
|
475
|
+
# * Hash - used to send multiple args or name an unnamed constraint
|
476
|
+
# # pass cc_no and cc_type in as arguments to cc_number constraint
|
477
|
+
# # and set {"cc_no" => ["cc_test"]} in failed hash if constraint fails.
|
478
|
+
# :cc_no => {
|
479
|
+
# :name => "cc_test",
|
480
|
+
# :constraint => :cc_number,
|
481
|
+
# :params => [:cc_no, :cc_type]
|
482
|
+
# }
|
483
|
+
#
|
484
|
+
# # If age coming in off the form is not all digits then set
|
485
|
+
# # {"age" => ["all_digits"]} in the failed hash.
|
486
|
+
# :age => {
|
487
|
+
# :name => "all_digits",
|
488
|
+
# :constraint => /^\d+$/
|
489
|
+
# }
|
490
|
+
#
|
491
|
+
# :constraints => { :age => /^1?\d{1,2}$/ }
|
492
|
+
# :constraints => { :zipcode => [:zip, /^\d+/],
|
493
|
+
# :fax => :american_phone,
|
494
|
+
# :email_addr => :email }
|
495
|
+
def constraints
|
496
|
+
return nil unless Hash === @profile[:constraints]
|
497
|
+
@profile[:constraints].each do |key,constraint|
|
498
|
+
do_constraint(key, [constraint].flatten) unless @form[key].to_s.empty?
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end # module InputProfile
|
502
|
+
|
503
|
+
module ConstraintHelpers
|
504
|
+
# Helper method to figure out what kind of constraint is being run.
|
505
|
+
# Valid constraint objects are String, Hash, Array, Proc, and Regexp.
|
506
|
+
def do_constraint(key, constraints)
|
507
|
+
constraints.each do |constraint|
|
508
|
+
type = constraint.type.to_s.intern
|
509
|
+
case type
|
510
|
+
when :String
|
511
|
+
apply_string_constraint(key, constraint)
|
512
|
+
when :Hash
|
513
|
+
apply_hash_constraint(key, constraint)
|
514
|
+
when :Proc
|
515
|
+
apply_proc_constraint(key, constraint)
|
516
|
+
when :Regexp
|
517
|
+
apply_regexp_constraint(key, constraint)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# Delete empty fields.
|
523
|
+
def delete_empty
|
524
|
+
@form.keys.each do |key|
|
525
|
+
@form.delete(key) if @form[key].to_s.empty?
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Find unknown fields and delete them from the form.
|
530
|
+
def delete_unknown
|
531
|
+
@unknown_fields =
|
532
|
+
@form.keys - @required_fields - @optional_fields - @require_some_fields
|
533
|
+
@unknown_fields.each {|field| @form.delete(field)}
|
534
|
+
end
|
535
|
+
|
536
|
+
# Indicates if @form[key] is scheduled to be untainted.
|
537
|
+
def untaint?(key)
|
538
|
+
@untaint_all || @untaint_fields.include?(key)
|
539
|
+
end
|
540
|
+
|
541
|
+
# Applies a builtin constraint to form[key]
|
542
|
+
def apply_string_constraint(key, constraint)
|
543
|
+
# FIXME: multiple elements
|
544
|
+
res = self.send("match_#{constraint}".intern, @form[key].to_s)
|
545
|
+
if res
|
546
|
+
if untaint?(key)
|
547
|
+
@form[key] = res
|
548
|
+
@form[key].untaint
|
549
|
+
end
|
550
|
+
else
|
551
|
+
@form.delete(key)
|
552
|
+
@invalid_fields[key] ||= []
|
553
|
+
unless @invalid_fields[key].include?(constraint)
|
554
|
+
@invalid_fields[key].push(constraint)
|
555
|
+
end
|
556
|
+
nil
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# Applies regexp constraint to form[key]
|
561
|
+
def apply_regexp_constraint(key, constraint)
|
562
|
+
# FIXME: multiple elements
|
563
|
+
m = constraint.match(@form[key].to_s)
|
564
|
+
if m
|
565
|
+
if untaint?(key)
|
566
|
+
@form[key] = m[0]
|
567
|
+
@form[key].untaint
|
568
|
+
end
|
569
|
+
else
|
570
|
+
@form.delete(key)
|
571
|
+
@invalid_fields[key] ||= []
|
572
|
+
unless @invalid_fields[key].include?(constraint.inspect)
|
573
|
+
@invalid_fields[key].push(constraint.inspect)
|
574
|
+
end
|
575
|
+
nil
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# applies a proc constraint to form[key]
|
580
|
+
def apply_proc_constraint(key, constraint)
|
581
|
+
if res = constraint.call(@form[key])
|
582
|
+
if untaint?(key)
|
583
|
+
@form[key] = res
|
584
|
+
@form[key].untaint
|
585
|
+
end
|
586
|
+
else
|
587
|
+
@form.delete(key)
|
588
|
+
@invalid_fields[key] ||= []
|
589
|
+
unless @invalid_fields[key].include?(constraint.inspect)
|
590
|
+
@invalid_fields[key].push(constraint.inspect)
|
591
|
+
end
|
592
|
+
nil
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
# A hash allows you to send multiple arguments to a constraint.
|
597
|
+
# constraint can be a builtin constraint, regexp, or a proc object.
|
598
|
+
# params is a list of form fields to be fed into the constraint or proc.
|
599
|
+
# If an optional name field is specified then it will be listed as
|
600
|
+
# the failed constraint in the invalid_fields hash.
|
601
|
+
def apply_hash_constraint(key, constraint)
|
602
|
+
name = constraint["name"]
|
603
|
+
action = constraint["constraint"]
|
604
|
+
params = constraint["params"]
|
605
|
+
res = false
|
606
|
+
|
607
|
+
# In order to call a builtin or proc, params and action must be present.
|
608
|
+
if action and params
|
609
|
+
arg = params.map {|m| @form[m]}
|
610
|
+
if String === action
|
611
|
+
res = self.send("match_#{action}".intern, *arg)
|
612
|
+
elsif Proc === action
|
613
|
+
res = action.call(*arg)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
if Regexp === action
|
618
|
+
# FIXME: multiple elements
|
619
|
+
m = action.match(@form[key].to_s)
|
620
|
+
res = m[0] if m
|
621
|
+
end
|
622
|
+
|
623
|
+
if res
|
624
|
+
@form[key] = res if untaint?(key)
|
625
|
+
else
|
626
|
+
@form.delete(key)
|
627
|
+
constraint = (name) ? name : constraint
|
628
|
+
@invalid_fields[key] ||= []
|
629
|
+
unless @invalid_fields[key].include?(constraint)
|
630
|
+
@invalid_fields[key].push(constraint)
|
631
|
+
end
|
632
|
+
nil
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end # module ConstraintHelpers
|
636
|
+
|
637
|
+
module Filters
|
638
|
+
# Remove white space at the front and end of the fields.
|
639
|
+
def filter_strip(value)
|
640
|
+
value.strip
|
641
|
+
end
|
642
|
+
|
643
|
+
# Runs of white space are replaced by a single space.
|
644
|
+
def filter_squeeze(value)
|
645
|
+
value.squeeze(" ")
|
646
|
+
end
|
647
|
+
|
648
|
+
# Remove non digits characters from the input.
|
649
|
+
def filter_digit(value)
|
650
|
+
value.gsub(/\D/, "")
|
651
|
+
end
|
652
|
+
|
653
|
+
# Remove non alphanumerical characters from the input.
|
654
|
+
def filter_alphanum(value)
|
655
|
+
value.gsub(/\W/, "")
|
656
|
+
end
|
657
|
+
|
658
|
+
# Extract from its input a valid integer number.
|
659
|
+
def filter_integer(value)
|
660
|
+
value.gsub(/[^\d+-]/, "")
|
661
|
+
end
|
662
|
+
|
663
|
+
# Extract from its input a valid positive integer number.
|
664
|
+
def filter_pos_integer(value)
|
665
|
+
value.gsub!(/[^\d+]/, "")
|
666
|
+
value.scan(/\+?\d+/).to_s
|
667
|
+
end
|
668
|
+
|
669
|
+
# Extract from its input a valid negative integer number.
|
670
|
+
def filter_neg_integer(value)
|
671
|
+
value.gsub!(/[^\d-]/, "")
|
672
|
+
value.scan(/\-?\d+/).to_s
|
673
|
+
end
|
674
|
+
|
675
|
+
# Extract from its input a valid decimal number.
|
676
|
+
def filter_decimal(value)
|
677
|
+
value.tr!(',', '.')
|
678
|
+
value.gsub!(/[^\d.+-]/, "")
|
679
|
+
value.scan(/([-+]?\d+\.?\d*)/).to_s
|
680
|
+
end
|
681
|
+
|
682
|
+
# Extract from its input a valid positive decimal number.
|
683
|
+
def filter_pos_decimal(value)
|
684
|
+
value.tr!(',', '.')
|
685
|
+
value.gsub!(/[^\d.+]/, "")
|
686
|
+
value.scan(/(\+?\d+\.?\d*)/).to_s
|
687
|
+
end
|
688
|
+
|
689
|
+
# Extract from its input a valid negative decimal number.
|
690
|
+
def filter_neg_decimal(value)
|
691
|
+
value.tr!(',', '.')
|
692
|
+
value.gsub!(/[^\d.-]/, "")
|
693
|
+
value.scan(/(-\d+\.?\d*)/).to_s
|
694
|
+
end
|
695
|
+
|
696
|
+
# Extract from its input a valid number to express dollars like currency.
|
697
|
+
def filter_dollars(value)
|
698
|
+
value.tr!(',', '.')
|
699
|
+
value.gsub!(/[^\d.+-]/, "")
|
700
|
+
value.scan(/(\d+\.?\d?\d?)/).to_s
|
701
|
+
end
|
702
|
+
|
703
|
+
# Filters out characters which aren't valid for an phone number. (Only
|
704
|
+
# accept digits [0-9], space, comma, minus, parenthesis, period and pound.
|
705
|
+
def filter_phone(value)
|
706
|
+
value.gsub(/[^\d,\(\)\.\s,\-#]/, "")
|
707
|
+
end
|
708
|
+
|
709
|
+
# Transforms shell glob wildcard (*) to the SQL like wildcard (%).
|
710
|
+
def filter_sql_wildcard(value)
|
711
|
+
value.tr('*', '%')
|
712
|
+
end
|
713
|
+
|
714
|
+
# Quotes special characters.
|
715
|
+
def filter_quote(value)
|
716
|
+
Regexp.quote(value)
|
717
|
+
end
|
718
|
+
|
719
|
+
# Calls the downcase method on its input.
|
720
|
+
def filter_downcase(value)
|
721
|
+
value.downcase
|
722
|
+
end
|
723
|
+
|
724
|
+
# Calls the upcase method on its input.
|
725
|
+
def filter_upcase(value)
|
726
|
+
value.upcase
|
727
|
+
end
|
728
|
+
|
729
|
+
# Calls the capitalize method on its input.
|
730
|
+
def filter_capitalize(value)
|
731
|
+
value.capitalize
|
732
|
+
end
|
733
|
+
end # module Filters
|
734
|
+
|
735
|
+
module Constraints
|
736
|
+
# Valid US state abbreviations.
|
737
|
+
STATES = [
|
738
|
+
:AL, :AK, :AZ, :AR, :CA, :CO, :CT, :DE, :FL, :GA, :HI, :ID, :IL, :IN,
|
739
|
+
:IA, :KS, :KY, :LA, :ME, :MD, :MA, :MI, :MN, :MS, :MO, :MT, :NE, :NV,
|
740
|
+
:NH, :NJ, :NM, :NY, :NC, :ND, :OH, :OK, :OR, :PA, :PR, :RI, :SC, :SD,
|
741
|
+
:TN, :TX, :UT, :VT, :VA, :WA, :WV, :WI, :WY, :DC, :AP, :FP, :FPO, :APO,
|
742
|
+
:GU, :VI ]
|
743
|
+
# Valid Canadian province abbreviations.
|
744
|
+
PROVINCES = [
|
745
|
+
:AB, :BC, :MB, :NB, :NF, :NS, :NT, :ON, :PE, :QC, :SK, :YT, :YK ]
|
746
|
+
|
747
|
+
# Sloppy matches a valid email address.
|
748
|
+
def match_email(email)
|
749
|
+
regexp = Regexp.new('^\S+@\w+(\.\w+)*$')
|
750
|
+
match = regexp.match(email)
|
751
|
+
match ? match[0] : nil
|
752
|
+
end
|
753
|
+
|
754
|
+
# Matches a US state or Canadian province.
|
755
|
+
def match_state_or_province(value)
|
756
|
+
match_state(value) || match_province(value)
|
757
|
+
end
|
758
|
+
|
759
|
+
# Matches a US state.
|
760
|
+
def match_state(state)
|
761
|
+
state = (state.type == String) ? state.intern : state
|
762
|
+
index = STATES.index(state)
|
763
|
+
(index) ? STATES[index].to_s : nil
|
764
|
+
end
|
765
|
+
|
766
|
+
# Matches a Canadian province.
|
767
|
+
def match_province(prov)
|
768
|
+
prov = (prov.type == String) ? prov.intern : prov
|
769
|
+
index = PROVINCES.index(prov)
|
770
|
+
(index) ? PROVINCES[index].to_s : nil
|
771
|
+
end
|
772
|
+
|
773
|
+
# Matches a Canadian postal code or US zipcode.
|
774
|
+
def match_zip_or_postcode(code)
|
775
|
+
match_zip(code) || match_postcode(code)
|
776
|
+
end
|
777
|
+
|
778
|
+
# Matches a Canadian postal code.
|
779
|
+
def match_postcode(code)
|
780
|
+
regexp = Regexp.new('^([ABCEGHJKLMNPRSTVXYabceghjklmnprstvxy]
|
781
|
+
[_\W]*\d[_\W]*[A-Za-z][_\W]*[- ]?[_\W]*\d[_\W]*
|
782
|
+
[A-Za-z][_\W]*\d[_\W]*)$', Regexp::EXTENDED)
|
783
|
+
match = regexp.match(code)
|
784
|
+
match ? match[0] : nil
|
785
|
+
end
|
786
|
+
|
787
|
+
# Matches a US zipcode.
|
788
|
+
def match_zip(code)
|
789
|
+
regexp = Regexp.new('^(\s*\d{5}(?:[-]\d{4})?\s*)$')
|
790
|
+
match = regexp.match(code)
|
791
|
+
match ? match[0] : nil
|
792
|
+
end
|
793
|
+
|
794
|
+
# Matches a generic phone number.
|
795
|
+
def match_phone(number)
|
796
|
+
regexp = Regexp.new('^(\D*\d\D*){6,}$')
|
797
|
+
match = regexp.match(number)
|
798
|
+
match ? match[0] : nil
|
799
|
+
end
|
800
|
+
|
801
|
+
# Matches a standard american phone number.
|
802
|
+
def match_american_phone(number)
|
803
|
+
regexp = Regexp.new('^(\D*\d\D*){7,}$')
|
804
|
+
match = regexp.match(number)
|
805
|
+
match ? match[0] : nil
|
806
|
+
end
|
807
|
+
|
808
|
+
# The number is checked only for plausibility, it checks if the number
|
809
|
+
# could be valid for a type of card by checking the checksum and looking at
|
810
|
+
# the number of digits and the number of digits of the number..
|
811
|
+
def match_cc_number(card, card_type)
|
812
|
+
orig_card = card
|
813
|
+
card_type = card_type.to_s
|
814
|
+
index = nil
|
815
|
+
digit = nil
|
816
|
+
multiplier = 2
|
817
|
+
sum = 0
|
818
|
+
return nil if card.length == 0
|
819
|
+
return nil unless card_type =~ /^[admv]/i
|
820
|
+
# Check the card type.
|
821
|
+
return nil if ((card_type =~ /^v/i && card[0,1] != "4") ||
|
822
|
+
(card_type =~ /^m/i && card[0,2] !~ /^51|55$/) ||
|
823
|
+
(card_type =~ /^d/i && card[0,4] !~ "6011") ||
|
824
|
+
(card_type =~ /^a/i && card[0,2] !~ /^34|37$/))
|
825
|
+
card.gsub!(" ", "")
|
826
|
+
return nil if card !~ /^\d+$/
|
827
|
+
digit = card[0,1]
|
828
|
+
index = (card.length-1)
|
829
|
+
# Check for the valid number of digits.
|
830
|
+
return nil if ((digit == "3" && index != 14) ||
|
831
|
+
(digit == "4" && index != 12 && index != 15) ||
|
832
|
+
(digit == "5" && index != 15) ||
|
833
|
+
(digit == "6" && index != 13 && index != 15))
|
834
|
+
(index-1).downto(0) do |i|
|
835
|
+
digit = card[i, 1].to_i
|
836
|
+
product = multiplier * digit
|
837
|
+
sum += (product > 9) ? (product-9) : product
|
838
|
+
multiplier = 3 - multiplier
|
839
|
+
end
|
840
|
+
sum %= 10
|
841
|
+
sum = 10 - sum unless sum == 0
|
842
|
+
if sum.to_s == card[-1,1]
|
843
|
+
match = /^([\d\s]*)$/.match(orig_card)
|
844
|
+
return match ? match[1] : nil
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
# This checks if the input is in the format MM/YY or MM/YYYY and if the MM
|
849
|
+
# part is a valid month (1-12) and if that date is not in the past.
|
850
|
+
def match_cc_exp(val)
|
851
|
+
matched_month = matched_year = nil
|
852
|
+
month, year = val.split("/")
|
853
|
+
return nil if (matched_month = month.scan(/^\d+$/).to_s).empty?
|
854
|
+
return nil if (matched_year = year.scan(/^\d+$/).to_s).empty?
|
855
|
+
year = year.to_i
|
856
|
+
month = month.to_i
|
857
|
+
year += (year < 70) ? 2000 : 1900 if year < 1900
|
858
|
+
now = Time.new.year
|
859
|
+
return nil if (year < now) || (year == now && month <= Time.new.month)
|
860
|
+
"#{matched_month}/#{matched_year}"
|
861
|
+
end
|
862
|
+
|
863
|
+
# This checks to see if the credit card type begins with a M, V, A, or D.
|
864
|
+
def match_cc_type(val)
|
865
|
+
(!val.scan(/^[MVAD].*$/i).empty?) ? val : nil
|
866
|
+
end
|
867
|
+
|
868
|
+
# This matches a valid IP address(version 4).
|
869
|
+
def match_ip_address(val)
|
870
|
+
regexp = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
|
871
|
+
match = regexp.match(val)
|
872
|
+
error = false
|
873
|
+
if match
|
874
|
+
1.upto(4) do |i|
|
875
|
+
error = true unless (match[i].to_i >= 0 && match[i].to_i <= 255)
|
876
|
+
end
|
877
|
+
else
|
878
|
+
error = true
|
879
|
+
end
|
880
|
+
error ? nil : match[0]
|
881
|
+
end
|
882
|
+
end # module Constraints
|
883
|
+
|
884
|
+
include InputProfile
|
885
|
+
include Filters
|
886
|
+
include Constraints
|
887
|
+
include ConstraintHelpers
|
888
|
+
end
|