formvalidator 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ require "../formvalidator"
2
+
3
+ form = {
4
+ "hello" => "werld"
5
+ }
6
+
7
+ fv = FormValidator.new("profiles/extension.rb")
8
+ fv.validate(form, :extension)
9
+ puts "hello " + form["hello"]
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,12 @@
1
+ module Filters
2
+ def filter_e_to_o(value)
3
+ value.gsub("e", "o")
4
+ end
5
+ end
6
+ {
7
+ :extension =>
8
+ {
9
+ :required => :hello,
10
+ :filters => :e_to_o
11
+ }
12
+ }
@@ -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
+ }
@@ -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