formvalidator 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|