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 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