strelka 0.0.3 → 0.1.0

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.
@@ -19,9 +19,9 @@ require 'strelka/app' unless defined?( Strelka::App )
19
19
  #
20
20
  # == Usage
21
21
  #
22
- # require 'strelka/app/formvalidator'
22
+ # require 'strelka/paramvalidator'
23
23
  #
24
- # validator = Strelka::ParamValidator.new
24
+ # validator = Strelka::ParamValidator.new
25
25
  #
26
26
  # # Add validation criteria for input parameters
27
27
  # validator.add( :name, /^(?<lastname>\S+), (?<firstname>\S+)$/, "Customer Name" )
@@ -29,921 +29,877 @@ require 'strelka/app' unless defined?( Strelka::App )
29
29
  # validator.add( :feedback, :printable, "Customer Feedback" )
30
30
  # validator.override( :email, :printable, "Your Email Address" )
31
31
  #
32
- # # Untaint all parameter values which match their constraints
33
- # validate.untaint_all_constraints = true
32
+ # # Untaint all parameter values which match their constraints
33
+ # validate.untaint_all_constraints = true
34
34
  #
35
35
  # # Now pass in tainted values in a hash (e.g., from an HTML form)
36
36
  # validator.validate( req.params )
37
37
  #
38
38
  # # Now if there weren't any errors, use some form values to fill out the
39
- # # success page template
39
+ # # success page template
40
40
  # if validator.okay?
41
41
  # tmpl = template :success
42
- # tmpl.firstname = validator[:name][:firstname]
43
- # tmpl.lastname = validator[:name][:lastname]
44
- # tmpl.email = validator[:email]
45
- # tmpl.feedback = validator[:feedback]
46
- # return tmpl
42
+ # tmpl.firstname = validator[:name][:firstname]
43
+ # tmpl.lastname = validator[:name][:lastname]
44
+ # tmpl.email = validator[:email]
45
+ # tmpl.feedback = validator[:feedback]
46
+ # return tmpl
47
47
  #
48
48
  # # Otherwise fill in the error template with auto-generated error messages
49
49
  # # and return that instead.
50
50
  # else
51
- # tmpl = template :feedback_form
51
+ # tmpl = template :feedback_form
52
52
  # tmpl.errors = validator.error_messages
53
53
  # return tmpl
54
54
  # end
55
55
  #
56
56
  class Strelka::ParamValidator < ::FormValidator
57
57
  extend Forwardable,
58
- Loggability
58
+ Loggability,
59
+ Strelka::MethodUtilities
60
+ include Strelka::DataUtilities
59
61
 
60
- # Loggability API -- set up logging under the 'strelka' log host
62
+ # Loggability API -- log to the 'strelka' logger
61
63
  log_to :strelka
62
64
 
63
65
 
64
- # Options that are passed as Symbols to .param
65
- FLAGS = [ :required, :untaint ]
66
-
67
- #
68
- # RFC822 Email Address Regex
69
- # --------------------------
70
- #
71
- # Originally written by Cal Henderson
72
- # c.f. http://iamcal.com/publish/articles/php/parsing_email/
73
- #
74
- # Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
75
- #
76
- # Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
77
- # http://creativecommons.org/licenses/by-sa/2.5/
78
- #
79
- RFC822_EMAIL_ADDRESS = begin
80
- qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
81
- dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
82
- atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
83
- '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
84
- quoted_pair = '\\x5c[\\x00-\\x7f]'
85
- domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
86
- quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
87
- domain_ref = atom
88
- sub_domain = "(?:#{domain_ref}|#{domain_literal})"
89
- word = "(?:#{atom}|#{quoted_string})"
90
- domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
91
- local_part = "#{word}(?:\\x2e#{word})*"
92
- addr_spec = "#{local_part}\\x40#{domain}"
93
- /\A#{addr_spec}\z/n
94
- end
95
-
96
- # Pattern for (loosely) matching a valid hostname. This isn't strictly RFC-compliant
97
- # because, in practice, many hostnames used on the Internet aren't.
98
- RFC1738_HOSTNAME = begin
99
- alphadigit = /[a-z0-9]/i
100
- # toplabel = alpha | alpha *[ alphadigit | "-" ] alphadigit
101
- toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
102
- # domainlabel = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
103
- domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
104
- # hostname = *[ domainlabel "." ] toplabel
105
- hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
106
- end
107
-
108
66
  # Pattern for countint the number of hash levels in a parameter key
109
67
  PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
110
68
 
111
- # The Hash of builtin constraints that are validated against a regular
112
- # expression.
113
- # :TODO: Document that these are the built-in constraints that can be used in a route
114
- BUILTIN_CONSTRAINT_PATTERNS = {
115
- :boolean => /^(?<boolean>t(?:rue)?|y(?:es)?|[10]|no?|f(?:alse)?)$/i,
116
- :integer => /^(?<integer>[\-\+]?\d+)$/,
117
- :float => /^(?<float>[\-\+]?(?:\d*\.\d+|\d+)(?:e[\-\+]?\d+)?)$/i,
118
- :alpha => /^(?<alpha>[[:alpha:]]+)$/,
119
- :alphanumeric => /^(?<alphanumeric>[[:alnum:]]+)$/,
120
- :printable => /\A(?<printable>[[:print:][:blank:]\r\n]+)\z/,
121
- :string => /\A(?<string>[[:print:][:blank:]\r\n]+)\z/,
122
- :word => /^(?<word>[[:word:]]+)$/,
123
- :email => /^(?<email>#{RFC822_EMAIL_ADDRESS})$/,
124
- :hostname => /^(?<hostname>#{RFC1738_HOSTNAME})$/,
125
- :uri => /^(?<uri>#{URI::URI_REF})$/,
126
- :uuid => /^(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})$/i
127
- }
128
-
129
69
  # Pattern to use to strip binding operators from parameter patterns so they
130
70
  # can be used in the middle of routing Regexps.
131
71
  PARAMETER_PATTERN_STRIP_RE = Regexp.union( '^', '$', '\\A', '\\z', '\\Z' )
132
72
 
133
73
 
134
74
 
135
- ### Return a Regex for the built-in constraint associated with the given +name+. If
136
- ### the builtin constraint is not pattern-based, or there is no such constraint,
137
- ### returns +nil+.
138
- def self::pattern_for_constraint( name )
139
- return BUILTIN_CONSTRAINT_PATTERNS[ name.to_sym ]
140
- end
75
+ # The base constraint type.
76
+ class Constraint
77
+ extend Loggability,
78
+ Strelka::MethodUtilities
141
79
 
80
+ # Loggability API -- log to the 'strelka' logger
81
+ log_to :strelka
142
82
 
143
- #################################################################
144
- ### I N S T A N C E M E T H O D S
145
- #################################################################
146
83
 
147
- ### Create a new Strelka::ParamValidator object.
148
- def initialize( profile={} )
149
- @profile = {
150
- descriptions: {},
151
- required: [],
152
- optional: [],
153
- descriptions: {},
154
- constraints: {},
155
- untaint_constraint_fields: [],
156
- }.merge( profile )
157
-
158
- @form = {}
159
- @raw_form = {}
160
- @invalid_fields = {}
161
- @missing_fields = []
162
- @unknown_fields = []
163
- @required_fields = []
164
- @require_some_fields = []
165
- @optional_fields = []
166
- @filters_array = []
167
- @untaint_fields = []
168
- @untaint_all = false
169
- @validated = false
170
-
171
- @parsed_params = nil
172
- end
84
+ # Flags that are passed as Symbols when declaring a parameter
85
+ FLAGS = [ :required, :untaint, :multiple ]
173
86
 
87
+ # Map of constraint specification types to their equivalent Constraint class.
88
+ TYPES = { Proc => self }
174
89
 
175
- ### Copy constructor.
176
- def initialize_copy( original )
177
- super
178
90
 
179
- @profile = original.profile.dup
180
- @profile.each_key {|k| @profile[k] = @profile[k].clone }
181
- self.log.debug "Copied validator profile: %p" % [ @profile ]
91
+ ### Register the given +subclass+ as the Constraint class to be used when
92
+ ### the specified +syntax_class+ is given as the constraint in a parameter
93
+ ### declaration.
94
+ def self::register_type( syntax_class )
95
+ self.log.debug "Registering %p as the constraint class for %p objects" %
96
+ [ self, syntax_class ]
97
+ TYPES[ syntax_class ] = self
98
+ end
182
99
 
183
- @form = @form.clone
184
- @raw_form = @form.clone
185
- @invalid_fields = @invalid_fields.clone
186
- @missing_fields = @missing_fields.clone
187
- @unknown_fields = @unknown_fields.clone
188
- @required_fields = @required_fields.clone
189
- @require_some_fields = @require_some_fields.clone
190
- @optional_fields = @optional_fields.clone
191
- @filters_array = @filters_array.clone
192
- @untaint_fields = @untaint_fields.clone
193
- @untaint_all = original.untaint_all?
194
- @validated = original.validated?
195
100
 
196
- @parsed_params = @parsed_params.clone if @parsed_params
197
- end
101
+ ### Return a Constraint object appropriate for the given +field+ and +spec+.
102
+ def self::for( field, spec=nil, *options, &block )
103
+ self.log.debug "Building Constraint for %p (%p)" % [ field, spec ]
198
104
 
105
+ # Handle omitted constraint
106
+ if spec.is_a?( String ) || FLAGS.include?( spec )
107
+ options.unshift( spec )
108
+ spec = nil
109
+ end
199
110
 
111
+ spec ||= block
200
112
 
201
- ######
202
- public
203
- ######
113
+ subtype = TYPES[ spec.class ] or
114
+ raise "No constraint type for a %p validation spec" % [ spec.class ]
204
115
 
205
- # The profile hash
206
- attr_reader :profile
116
+ return subtype.new( field, spec, *options, &block )
117
+ end
207
118
 
208
- # The raw form data Hash
209
- attr_reader :raw_form
210
119
 
211
- # The validated form data Hash
212
- attr_reader :form
120
+ ### Create a new Constraint for the field with the given +name+, configuring it with the
121
+ ### specified +args+. The +block+ is what does the actual validation, at least in the
122
+ ### base class.
123
+ def initialize( name, *args, &block )
124
+ @name = name
125
+ @block = block
213
126
 
214
- # Global untainting flag
215
- attr_accessor :untaint_all
216
- alias_method :untaint_all_constraints=, :untaint_all=
217
- alias_method :untaint_all?, :untaint_all
218
- alias_method :untaint_all_constraints, :untaint_all
219
- alias_method :untaint_all_constraints?, :untaint_all
127
+ @description = args.shift if args.first.is_a?( String )
220
128
 
129
+ @required = args.include?( :required )
130
+ @untaint = args.include?( :untaint )
131
+ @multiple = args.include?( :multiple )
132
+ end
221
133
 
222
134
 
223
- ### Return the Array of declared parameter validations.
224
- def param_names
225
- return self.profile[:required] | self.profile[:optional]
226
- end
135
+ ######
136
+ public
137
+ ######
227
138
 
139
+ # The name of the field the constraint governs
140
+ attr_reader :name
228
141
 
229
- ### Fetch the constraint/s that apply to the parameter with the given
230
- ### +name+.
231
- def constraint_for( name )
232
- constraint = self.profile[:constraints][ name.to_s ] or
233
- raise ScriptError, "no parameter %p defined" % [ name ]
234
- return constraint
235
- end
142
+ # The constraint's check block
143
+ attr_reader :block
236
144
 
145
+ # The field's description
146
+ attr_writer :description
237
147
 
238
- ### Fetch the constraint/s that apply to the parameter named +name+ as a
239
- ### Regexp, if possible.
240
- def constraint_regexp_for( name )
241
- self.log.debug " searching for a constraint for %p" % [ name ]
148
+ ##
149
+ # Returns true if the field can have multiple values.
150
+ attr_predicate :multiple?
242
151
 
243
- # Munge the constraint into a Regexp
244
- constraint = self.constraint_for( name )
245
- re = case constraint
246
- when Regexp
247
- self.log.debug " regex constraint is: %p" % [ constraint ]
248
- constraint
249
- when Array
250
- sub_res = constraint.map( &self.method(:extract_route_from_constraint) )
251
- Regexp.union( sub_res )
252
- when Symbol
253
- self.class.pattern_for_constraint( constraint ) or
254
- raise ScriptError, "no pattern for built-in %p constraint" % [ constraint ]
255
- else
256
- raise ScriptError,
257
- "can't route on a parameter with a %p constraint %p" % [ constraint.class ]
258
- end
152
+ ##
153
+ # Returns true if the field associated with the constraint is required in
154
+ # order for the parameters to be valid.
155
+ attr_predicate :required?
259
156
 
260
- self.log.debug " bounded constraint is: %p" % [ re ]
157
+ ##
158
+ # Returns true if the constraint will also untaint its result before returning it.
159
+ attr_predicate :untaint?
261
160
 
262
- # Unbind the pattern from beginning or end of line.
263
- # :TODO: This is pretty ugly. Find a better way of modifying the regex.
264
- re_str = re.to_s.
265
- sub( %r{\(\?[\-mix]+:(.*)\)}, '\\1' ).
266
- gsub( PARAMETER_PATTERN_STRIP_RE, '' )
267
- self.log.debug " stripped constraint pattern down to: %p" % [ re_str ]
268
161
 
269
- return Regexp.new( "(?<#{name}>#{re_str})", re.options )
270
- end
162
+ ### Check the given value against the constraint and return the result if it passes.
163
+ def apply( value, force_untaint=false )
164
+ untaint = self.untaint? || force_untaint
271
165
 
166
+ if self.multiple?
167
+ return self.check_multiple( value, untaint )
168
+ else
169
+ return self.check( value, untaint )
170
+ end
171
+ end
272
172
 
273
- ### :call-seq:
274
- ### param( name, *flags )
275
- ### param( name, constraint, *flags )
276
- ### param( name, description, *flags )
277
- ### param( name, constraint, description, *flags )
278
- ###
279
- ### Add a validation for a parameter with the specified +name+. The +args+ can include
280
- ### a constraint, a description, and one or more flags.
281
- def add( name, *args, &block )
282
- name = name.to_s
283
- constraint = self.make_param_validator( name, args, &block )
284
173
 
285
- # No-op if there's already a parameter with the same name and constraint
286
- if self.param_names.include?( name )
287
- return if self.profile[:constraints][ name ] == constraint
288
- raise ArgumentError,
289
- "parameter %p is already defined as a '%s'; perhaps you meant to use #override?" %
290
- [ name, self.profile[:constraints][name] ]
174
+ ### Comparison operator Constraints are equal if they’re for the same field,
175
+ ### they’re of the same type, and their blocks are the same.
176
+ def ==( other )
177
+ return self.name == other.name &&
178
+ other.instance_of?( self.class ) &&
179
+ self.block == other.block
291
180
  end
292
181
 
293
- self.log.debug "Adding parameter '%s' to profile" % [ name ]
294
- self.set_param( name, constraint, *args, &block )
295
- end
296
182
 
183
+ ### Get the description of the field.
184
+ def description
185
+ return @description || self.generate_description
186
+ end
297
187
 
298
- ### Replace the existing parameter with the specified +name+. The +args+ replace
299
- ### the existing description, constraints, and flags. See #add for details.
300
- def override( name, *args, &block )
301
- name = name.to_s
302
- raise ArgumentError,
303
- "no parameter %p defined; perhaps you meant to use #add?" % [name] unless
304
- self.param_names.include?( name )
305
188
 
306
- constraint = self.make_param_validator( name, args, &block )
307
- self.log.debug "Overriding parameter '%s' in profile" % [ name ]
308
- self.set_param( name, constraint, *args, &block )
309
- end
189
+ ### Return the constraint expressed as a String.
190
+ def to_s
191
+ desc = self.validator_description
310
192
 
193
+ flags = []
194
+ flags << 'required' if self.required?
195
+ flags << 'multiple' if self.multiple?
196
+ flags << 'untaint' if self.untaint?
311
197
 
312
- ### Extract a validator from the given +args+ and return it.
313
- def make_param_validator( name, args, &block )
314
- self.log.debug "Finding param validator out of: %s, %p, %p" % [ name, args, block ]
315
- args.unshift( block ) if block
198
+ desc << " (%s)" % [ flags.join(',') ] unless flags.empty?
316
199
 
317
- # Custom validator -- either a callback or a regex
318
- if args.first.is_a?( Regexp ) || args.first.respond_to?( :call )
319
- return args.shift
200
+ return desc
201
+ end
320
202
 
321
- # Builtin match validator, either explicit or implied by the name
322
- else
323
- return args.shift if args.first.is_a?( Symbol ) && !FLAGS.include?( args.first )
324
203
 
325
- raise ArgumentError, "no builtin %p validator" % [ name ] unless
326
- self.respond_to?( "match_#{name}" )
327
- return name
204
+ #########
205
+ protected
206
+ #########
207
+
208
+ ### Return a description of the validation provided by the constraint object.
209
+ def validator_description
210
+ desc = 'a custom validator'
211
+
212
+ if self.block
213
+ location = self.block.source_location
214
+ desc << " on line %d of %s" % [ location[1], location[0] ]
215
+ end
216
+
217
+ return desc
328
218
  end
329
219
 
330
- end
331
220
 
221
+ ### Check the specified value against the constraint and return the results. By
222
+ ### default, this just calls to_proc and the block and calls the result with the
223
+ ### value as its argument.
224
+ def check( value, untaint )
225
+ return self.block.to_proc.call( value ) if self.block
226
+ value.untaint if untaint && value.respond_to?( :untaint )
227
+ return value
228
+ end
332
229
 
333
- ### Stringified description of the validator
334
- def to_s
335
- "%d parameters (%d valid, %d invalid, %d missing)" % [
336
- self.raw_form.size,
337
- self.form.size,
338
- self.invalid.size,
339
- self.missing.size,
340
- ]
341
- end
342
230
 
231
+ ### Check the given +values+ against the constraint and return the results if
232
+ ### all of them succeed.
233
+ def check_multiple( values, untaint )
234
+ values = [ values ] unless values.is_a?( Array )
235
+ results = []
343
236
 
344
- ### Return a human-readable representation of the validator, suitable for debugging.
345
- def inspect
346
- required = self.profile[:required].collect do |field|
347
- "%s (%p)" % [ field, self.profile[:constraints][field] ]
348
- end.join( ',' )
349
- optional = self.profile[:optional].collect do |field|
350
- "%s (%p)" % [ field, self.profile[:constraints][field] ]
351
- end.join( ',' )
237
+ values.each do |value|
238
+ result = self.check( value, untaint ) or return nil
239
+ results << result
240
+ end
352
241
 
353
- return "#<%p:0x%016x %s, profile: [required: %s, optional: %s] global untaint: %s>" % [
354
- self.class,
355
- self.object_id / 2,
356
- self.to_s,
357
- required.empty? ? "(none)" : required,
358
- optional.empty? ? "(none)" : optional,
359
- self.untaint_all? ? "enabled" : "disabled",
360
- ]
361
- end
242
+ return results
243
+ end
362
244
 
363
245
 
364
- ### Hash of field descriptions
365
- def descriptions
366
- return @profile[:descriptions]
367
- end
246
+ ### Generate a description from the name of the field.
247
+ def generate_description
248
+ self.log.debug "Auto-generating description for %p" % [ self ]
249
+ desc = self.name.to_s.
250
+ gsub( /.*\[(\w+)\]/, "\\1" ).
251
+ gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
252
+ gsub( /^(.)/ ) {|m| m.upcase }
253
+ self.log.debug " generated: %p" % [ desc ]
254
+ return desc
255
+ end
368
256
 
257
+ end # class Constraint
369
258
 
370
- ### Set hash of field descriptions
371
- def descriptions=( new_descs )
372
- return @profile[:descriptions] = new_descs
373
- end
374
259
 
260
+ # A constraint expressed as a regular expression.
261
+ class RegexpConstraint < Constraint
262
+
263
+ # Use this for constraints expressed as Regular Expressions
264
+ register_type Regexp
375
265
 
376
- ### Validate the input in +params+. If the optional +additional_profile+ is
377
- ### given, merge it with the validator's default profile before validating.
378
- def validate( params=nil, additional_profile=nil )
379
- params ||= {}
380
266
 
381
- self.log.info "Validating request params: %p with profile: %p" %
382
- [ params, @profile ]
383
- @raw_form = strify_hash( params )
384
- profile = @profile
267
+ ### Create a new RegexpConstraint that will validate the field of the given
268
+ ### +name+ with the specified +pattern+.
269
+ def initialize( name, pattern, *args, &block )
270
+ @pattern = pattern
385
271
 
386
- if additional_profile
387
- self.log.debug " merging additional profile %p" % [ additional_profile ]
388
- profile = @profile.merge( additional_profile )
272
+ super( name, *args, &block )
389
273
  end
390
274
 
391
- self.log.debug "Calling superclass's validate: %p" % [ self ]
392
- super( params, profile )
393
- @validated = true
394
- end
395
275
 
276
+ ######
277
+ public
278
+ ######
396
279
 
397
- protected :convert_profile
280
+ # The constraint's pattern
281
+ attr_reader :pattern
398
282
 
399
- # Load profile with a hash describing valid input.
400
- def setup(form_data, profile)
401
- @form = form_data
402
- @profile = self.convert_profile( @profile )
403
- end
404
283
 
284
+ ### Check the +value+ against the regular expression and return its
285
+ ### match groups if successful.
286
+ def check( value, untaint )
287
+ self.log.debug "Validating %p via regexp %p" % [ value, self.pattern ]
288
+ match = self.pattern.match( value.to_s ) or return nil
405
289
 
406
- ### Set the parameter +name+ in the profile to validate using the given +args+,
407
- ### which are the same as the ones passed to #add and #override.
408
- def set_param( name, validator, *args, &block )
409
- self.profile[:constraints][ name ] = validator
290
+ if match.captures.empty?
291
+ self.log.debug " no captures, using whole match: %p" % [match[0]]
292
+ return super( match[0], untaint )
410
293
 
411
- self.profile[:descriptions][ name ] = args.shift if args.first.is_a?( String )
294
+ elsif match.names.length > 1
295
+ self.log.debug " extracting hash of named captures: %p" % [ match.names ]
296
+ rhash = self.matched_hash( match, untaint )
297
+ return super( rhash, untaint )
412
298
 
413
- if args.include?( :required )
414
- self.profile[:required] |= [ name ]
415
- self.profile[:optional].delete( name )
416
- else
417
- self.profile[:required].delete( name )
418
- self.profile[:optional] |= [ name ]
419
- end
299
+ elsif match.captures.length == 1
300
+ self.log.debug " extracting one capture: %p" % [match.captures.first]
301
+ return super( match.captures.first, untaint )
420
302
 
421
- if args.include?( :untaint )
422
- self.profile[:untaint_constraint_fields] |= [ name ]
423
- else
424
- self.profile[:untaint_constraint_fields].delete( name )
303
+ else
304
+ self.log.debug " extracting multiple captures: %p" % [match.captures]
305
+ values = match.captures
306
+ values.map {|val| val.untaint if val } if untaint
307
+ return super( values, untaint )
308
+ end
425
309
  end
426
310
 
427
- self.revalidate if self.validated?
428
- end
429
311
 
312
+ ### Return a Hash of the given +match+ object's named captures, untainting the values
313
+ ### if +untaint+ is true.
314
+ def matched_hash( match, untaint )
315
+ return match.names.inject( {} ) do |accum,name|
316
+ value = match[ name ]
317
+ value.untaint if untaint && value
318
+ accum[ name.to_sym ] = value
319
+ accum
320
+ end
321
+ end
430
322
 
431
- ### Overridden to remove the check for extra keys.
432
- def check_profile_syntax( profile )
433
- end
434
323
 
324
+ ### Return the constraint expressed as a String.
325
+ def validator_description
326
+ return "a value matching the pattern %p" % [ self.pattern ]
327
+ end
435
328
 
436
- ### Index operator; fetch the validated value for form field +key+.
437
- def []( key )
438
- return @form[ key.to_s ]
439
- end
440
329
 
330
+ end # class RegexpConstraint
331
+
332
+
333
+ # A constraint class that uses a collection of predefined patterns.
334
+ class BuiltinConstraint < RegexpConstraint
335
+
336
+ # Use this for constraints expressed as Symbols or who are missing a constraint spec (nil)
337
+ register_type Symbol
338
+ register_type NilClass
339
+
340
+
341
+ #
342
+ # RFC822 Email Address Regex
343
+ # --------------------------
344
+ #
345
+ # Originally written by Cal Henderson
346
+ # c.f. http://iamcal.com/publish/articles/php/parsing_email/
347
+ #
348
+ # Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
349
+ #
350
+ # Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
351
+ # http://creativecommons.org/licenses/by-sa/2.5/
352
+ #
353
+ RFC822_EMAIL_ADDRESS = begin
354
+ qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
355
+ dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
356
+ atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
357
+ '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
358
+ quoted_pair = '\\x5c[\\x00-\\x7f]'
359
+ domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
360
+ quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
361
+ domain_ref = atom
362
+ sub_domain = "(?:#{domain_ref}|#{domain_literal})"
363
+ word = "(?:#{atom}|#{quoted_string})"
364
+ domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
365
+ local_part = "#{word}(?:\\x2e#{word})*"
366
+ addr_spec = "#{local_part}\\x40#{domain}"
367
+ /\A#{addr_spec}\z/n
368
+ end
441
369
 
442
- ### Index assignment operator; set the validated value for form field +key+
443
- ### to the specified +val+.
444
- def []=( key, val )
445
- return @form[ key.to_s ] = val
446
- end
370
+ # Pattern for (loosely) matching a valid hostname. This isn't strictly RFC-compliant
371
+ # because, in practice, many hostnames used on the Internet aren't.
372
+ RFC1738_HOSTNAME = begin
373
+ alphadigit = /[a-z0-9]/i
374
+ # toplabel = alpha | alpha *[ alphadigit | "-" ] alphadigit
375
+ toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
376
+ # domainlabel = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
377
+ domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
378
+ # hostname = *[ domainlabel "." ] toplabel
379
+ hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
380
+ end
447
381
 
382
+ # The Hash of builtin constraints that are validated against a regular
383
+ # expression.
384
+ # :TODO: Document that these are the built-in constraints that can be used in a route
385
+ BUILTIN_CONSTRAINT_PATTERNS = {
386
+ :boolean => /^(?<boolean>t(?:rue)?|y(?:es)?|[10]|no?|f(?:alse)?)$/i,
387
+ :integer => /^(?<integer>[\-\+]?\d+)$/,
388
+ :float => /^(?<float>[\-\+]?(?:\d*\.\d+|\d+)(?:e[\-\+]?\d+)?)$/i,
389
+ :alpha => /^(?<alpha>[[:alpha:]]+)$/,
390
+ :alphanumeric => /^(?<alphanumeric>[[:alnum:]]+)$/,
391
+ :printable => /\A(?<printable>[[:print:][:blank:]\r\n]+)\z/,
392
+ :string => /\A(?<string>[[:print:][:blank:]\r\n]+)\z/,
393
+ :word => /^(?<word>[[:word:]]+)$/,
394
+ :email => /^(?<email>#{RFC822_EMAIL_ADDRESS})$/,
395
+ :hostname => /^(?<hostname>#{RFC1738_HOSTNAME})$/,
396
+ :uri => /^(?<uri>#{URI::URI_REF})$/,
397
+ :uuid => /^(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})$/i,
398
+ :date => /.*\d.*/,
399
+ }
400
+
401
+ # Field values which result in a valid ‘true’ value for :boolean constraints
402
+ TRUE_VALUES = %w[t true y yes 1]
403
+
404
+
405
+ ### Return true if name is the name of a built-in constraint.
406
+ def self::valid?( name )
407
+ return BUILTIN_CONSTRAINT_PATTERNS.key?( name.to_sym )
408
+ end
448
409
 
449
- ### Returns +true+ if there were no arguments given.
450
- def empty?
451
- return @form.empty?
452
- end
453
410
 
411
+ ### Create a new BuiltinConstraint using the pattern named name for the specified field.
412
+ def initialize( field, name, *options, &block )
413
+ name ||= field
414
+ @pattern_name = name
415
+ pattern = BUILTIN_CONSTRAINT_PATTERNS[ name.to_sym ] or
416
+ raise ScriptError, "no such builtin constraint %p" % [ name ]
454
417
 
455
- ### Returns +true+ if there were arguments given.
456
- def args?
457
- return !@form.empty?
458
- end
459
- alias_method :has_args?, :args?
418
+ super( field, pattern, *options, &block )
419
+ end
460
420
 
461
421
 
462
- ### Returns +true+ if the parameters have been validated.
463
- def validated?
464
- return @validated
465
- end
422
+ ######
423
+ public
424
+ ######
466
425
 
426
+ # The name of the builtin pattern the field should be constrained by
427
+ attr_reader :pattern_name
467
428
 
468
- ### Returns +true+ if any fields are missing or contain invalid values.
469
- def errors?
470
- return !self.okay?
471
- end
472
- alias_method :has_errors?, :errors?
473
429
 
430
+ ### Check for an additional post-processor method, and if it exists, return it as
431
+ ### a Method object.
432
+ def block
433
+ if custom_block = super
434
+ return custom_block
435
+ else
436
+ post_processor = "post_process_%s" % [ @pattern_name ]
437
+ return nil unless self.respond_to?( post_processor )
438
+ return self.method( post_processor )
439
+ end
440
+ end
474
441
 
475
- ### Return +true+ if all required fields were present and validated
476
- ### correctly.
477
- def okay?
478
- return (self.missing.empty? && self.invalid.empty?)
479
- end
480
442
 
443
+ ### Return the constraint expressed as a String.
444
+ def validator_description
445
+ return "a '%s'" % [ self.pattern_name ]
446
+ end
481
447
 
482
- ### Returns +true+ if the given +field+ is one that should be untainted.
483
- def untaint?( field )
484
- self.log.debug "Checking to see if %p should be untainted." % [field]
485
- rval = ( self.untaint_all? ||
486
- @untaint_fields.include?(field) ||
487
- @untaint_fields.include?(field.to_sym) )
488
448
 
489
- if rval
490
- self.log.debug " ...yep it should."
491
- else
492
- self.log.debug " ...nope; untaint_all is: %p, untaint fields is: %p" %
493
- [ @untaint_all, @untaint_fields ]
449
+ #########
450
+ protected
451
+ #########
452
+
453
+ ### Post-process a :boolean value.
454
+ def post_process_boolean( val )
455
+ return TRUE_VALUES.include?( val.to_s.downcase )
494
456
  end
495
457
 
496
- return rval
497
- end
498
458
 
459
+ ### Constrain a value to a parseable Date
460
+ def post_process_date( val )
461
+ return Date.parse( val )
462
+ rescue ArgumentError
463
+ return nil
464
+ end
499
465
 
500
- ### Return an array of field names which had some kind of error associated
501
- ### with them.
502
- def error_fields
503
- return self.missing | self.invalid.keys
504
- end
505
466
 
467
+ ### Constrain a value to a Float
468
+ def post_process_float( val )
469
+ return Float( val.to_s )
470
+ end
506
471
 
507
- ### Get the description for the specified field.
508
- def get_description( field )
509
- return @profile[:descriptions][ field.to_s ] if
510
- @profile[:descriptions].key?( field.to_s )
511
472
 
512
- desc = field.to_s.
513
- gsub( /.*\[(\w+)\]/, "\\1" ).
514
- gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
515
- gsub( /^(.)/ ) {|m| m.upcase }
516
- return desc
517
- end
473
+ ### Post-process a valid :integer field.
474
+ def post_process_integer( val )
475
+ return Integer( val.to_s )
476
+ end
518
477
 
519
478
 
520
- ### Return an error message for each missing or invalid field; if
521
- ### +includeUnknown+ is +true+, also include messages for unknown fields.
522
- def error_messages( include_unknown=false )
523
- self.log.debug "Building error messages from descriptions: %p" %
524
- [ @profile[:descriptions] ]
525
- msgs = []
526
- self.missing.each do |field|
527
- msgs << "Missing value for '%s'" % self.get_description( field )
479
+ ### Post-process a valid :uri field.
480
+ def post_process_uri( val )
481
+ return URI.parse( val.to_s )
482
+ rescue URI::InvalidURIError => err
483
+ self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
484
+ return nil
485
+ rescue NoMethodError
486
+ self.log.debug "Ignoring bug in URI#parse"
487
+ return nil
528
488
  end
529
489
 
530
- self.invalid.each do |field, constraint|
531
- msgs << "Invalid value for '%s'" % self.get_description( field )
532
- end
490
+ end # class BuiltinConstraint
533
491
 
534
- if include_unknown
535
- self.unknown.each do |field|
536
- msgs << "Unknown parameter '%s'" % self.get_description( field )
537
- end
538
- end
539
492
 
540
- return msgs
493
+
494
+ #################################################################
495
+ ### I N S T A N C E M E T H O D S
496
+ #################################################################
497
+
498
+ ### Create a new Strelka::ParamValidator object.
499
+ def initialize
500
+ @constraints = {}
501
+ @fields = {}
502
+ @untaint_all = false
503
+
504
+ self.reset
541
505
  end
542
506
 
543
507
 
544
- ### Returns a distinct list of missing fields. Overridden to eliminate the
545
- ### "undefined method `<=>' for :foo:Symbol" error.
546
- def missing
547
- @missing_fields.uniq.sort_by {|f| f.to_s}
508
+ ### Copy constructor.
509
+ def initialize_copy( original )
510
+ fields = deep_copy( original.fields )
511
+ self.reset
512
+ @fields = fields
513
+ @constraints = deep_copy( original.constraints )
548
514
  end
549
515
 
550
- ### Returns a distinct list of unknown fields.
551
- def unknown
552
- (@unknown_fields - @invalid_fields.keys).uniq.sort_by {|f| f.to_s}
516
+
517
+ ######
518
+ public
519
+ ######
520
+
521
+ # The constraints hash
522
+ attr_reader :constraints
523
+
524
+ # The Hash of raw field data (if validation has occurred)
525
+ attr_reader :fields
526
+
527
+ ##
528
+ # Global untainting flag
529
+ attr_predicate_accessor :untaint_all?
530
+ alias_method :untaint_all_constraints=, :untaint_all=
531
+ alias_method :untaint_all_constraints?, :untaint_all?
532
+
533
+ ##
534
+ # Returns +true+ if the paramvalidator has been given parameters to validate. Adding or
535
+ # overriding constraints resets this.
536
+ attr_predicate_accessor :validated?
537
+
538
+
539
+ ### Reset the validation state.
540
+ def reset
541
+ self.log.debug "Resetting validation state."
542
+ @validated = false
543
+ @valid = {}
544
+ @parsed_params = nil
545
+ @missing = []
546
+ @unknown = []
547
+ @invalid = {}
553
548
  end
554
549
 
555
550
 
556
- ### Returns the valid fields after expanding Rails-style
557
- ### 'customer[address][street]' variables into multi-level hashes.
558
- def valid
559
- if @parsed_params.nil?
560
- @parsed_params = {}
561
- valid = super()
551
+ ### :call-seq:
552
+ ### add( name, *flags )
553
+ ### add( name, constraint, *flags )
554
+ ### add( name, description, *flags )
555
+ ### add( name, constraint, description, *flags )
556
+ ###
557
+ ### Add a validation for a parameter with the specified +name+. The +args+ can include
558
+ ### a constraint, a description, and one or more flags.
559
+ def add( name, *args, &block )
560
+ name = name.to_sym
561
+ constraint = Constraint.for( name, *args, &block )
562
562
 
563
- for key, value in valid
564
- value = [ value ] if key.end_with?( '[]' )
565
- if key.include?( '[' )
566
- build_deep_hash( value, @parsed_params, get_levels(key) )
567
- else
568
- @parsed_params[ key ] = value
569
- end
570
- end
563
+ # No-op if there's already a parameter with the same name and constraint
564
+ if self.constraints.key?( name )
565
+ return if self.constraints[ name ] == constraint
566
+ raise ArgumentError,
567
+ "parameter %p is already defined as %s; perhaps you meant to use #override?" %
568
+ [ name.to_s, self.constraints[name] ]
571
569
  end
572
570
 
573
- return @parsed_params
571
+ self.log.debug "Adding parameter %p: %p" % [ name, constraint ]
572
+ self.constraints[ name ] = constraint
573
+
574
+ self.validated = false
574
575
  end
575
576
 
576
577
 
577
- ### Return a new ParamValidator with the additional +params+ merged into
578
- ### its values and re-validated.
579
- def merge( params )
580
- copy = self.dup
581
- copy.merge!( params )
582
- return copy
583
- end
578
+ ### Replace the existing parameter with the specified name. The args replace the
579
+ ### existing description, constraints, and flags. See #add for details.
580
+ def override( name, *args, &block )
581
+ name = name.to_sym
582
+ raise ArgumentError,
583
+ "no parameter %p defined; perhaps you meant to use #add?" % [ name.to_s ] unless
584
+ self.constraints.key?( name )
584
585
 
586
+ self.log.debug "Overriding parameter %p" % [ name ]
587
+ self.constraints[ name ] = Constraint.for( name, *args, &block )
585
588
 
586
- ### Merge the specified +params+ into the receiving ParamValidator and
587
- ### re-validate the resulting values.
588
- def merge!( params )
589
- return if params.empty?
590
- self.log.debug "Merging parameters for revalidation: %p" % [ params ]
591
- self.revalidate( params )
589
+ self.validated = false
592
590
  end
593
591
 
594
592
 
595
- ### Clear existing validation information and re-check against the
596
- ### current state of the profile.
597
- def revalidate( params={} )
598
- @missing_fields.clear
599
- @unknown_fields.clear
600
- @required_fields.clear
601
- @invalid_fields.clear
602
- @untaint_fields.clear
603
- @require_some_fields.clear
604
- @optional_fields.clear
605
- @form.clear
593
+ ### Return the Array of parameter names the validator knows how to validate (as Strings).
594
+ def param_names
595
+ return self.constraints.keys.map( &:to_s ).sort
596
+ end
606
597
 
607
- newparams = @raw_form.merge( params )
608
- @raw_form.clear
609
598
 
610
- self.log.debug " merged raw form is: %p" % [ newparams ]
611
- self.validate( newparams )
599
+ ### Stringified description of the validator
600
+ def to_s
601
+ "%d parameters (%d valid, %d invalid, %d missing)" % [
602
+ self.fields.size,
603
+ self.valid.size,
604
+ self.invalid.size,
605
+ self.missing.size,
606
+ ]
612
607
  end
613
608
 
614
609
 
615
- ### Returns an array containing valid parameters in the validator corresponding to the
616
- ### given +selector+(s).
617
- def values_at( *selector )
618
- selector.map!( &:to_s )
619
- return @form.values_at( *selector )
620
- end
621
-
622
-
623
- #########
624
- protected
625
- #########
626
-
627
- #
628
- # :section: Builtin Match Constraints
629
- #
630
-
631
- ### Try to match the specified +val+ using the built-in constraint pattern
632
- ### associated with +name+, returning the matched value upon success, and +nil+
633
- ### if the +val+ didn't match. If a +block+ is given, it's called with the
634
- ### associated MatchData on success, and its return value is returned instead of
635
- ### the matching String.
636
- def match_builtin_constraint( val, name )
637
- self.log.debug "Validating %p using built-in constraint %p" % [ val, name ]
638
- re = self.class.pattern_for_constraint( name.to_sym )
639
- match = re.match( val ) or return nil
640
- self.log.debug " matched: %p" % [ match ]
641
-
642
- if block_given?
643
- begin
644
- return yield( match )
645
- rescue ArgumentError
646
- return nil
647
- end
648
- else
649
- return match.to_s
610
+ ### Return a human-readable representation of the validator, suitable for debugging.
611
+ def inspect
612
+ required, optional = self.constraints.partition do |_, constraint|
613
+ constraint.required?
650
614
  end
615
+
616
+ return "#<%p:0x%016x %s, profile: [required: %s, optional: %s] global untaint: %s>" % [
617
+ self.class,
618
+ self.object_id / 2,
619
+ self.to_s,
620
+ required.empty? ? "(none)" : required.map( &:last ).map( &:name ).join(','),
621
+ optional.empty? ? "(none)" : optional.map( &:last ).map( &:name ).join(','),
622
+ self.untaint_all? ? "enabled" : "disabled",
623
+ ]
651
624
  end
652
625
 
653
626
 
654
- ### Constrain a value to +true+ (or +yes+) and +false+ (or +no+).
655
- def match_boolean( val )
656
- return self.match_builtin_constraint( val, :boolean ) do |m|
657
- m.to_s.start_with?( 'y', 't', '1' )
627
+ ### Hash of field descriptions
628
+ def descriptions
629
+ return self.constraints.each_with_object({}) do |(field,constraint), hash|
630
+ hash[ field ] = constraint.description
658
631
  end
659
632
  end
660
633
 
661
634
 
662
- ### Constrain a value to an integer
663
- def match_integer( val )
664
- return self.match_builtin_constraint( val, :integer ) do |m|
665
- Integer( m.to_s )
635
+ ### Set field descriptions en masse to new_descs.
636
+ def descriptions=( new_descs )
637
+ new_descs.each do |name, description|
638
+ raise NameError, "no parameter named #{name}" unless
639
+ self.constraints.key?( name.to_sym )
640
+ self.constraints[ name.to_sym ].description = description
666
641
  end
667
642
  end
668
643
 
669
644
 
670
- ### Contrain a value to a Float
671
- def match_float( val )
672
- return self.match_builtin_constraint( val, :float ) do |m|
673
- Float( m.to_s )
645
+ ### Get the description for the specified +field+.
646
+ def get_description( field )
647
+ constraint = self.constraints[ field.to_sym ] or return nil
648
+ return constraint.description
649
+ end
650
+
651
+
652
+ ### Validate the input in +params+. If the optional +additional_constraints+ is
653
+ ### given, merge it with the validator's existing constraints before validating.
654
+ def validate( params=nil, additional_constraints=nil )
655
+ self.log.debug "Validating."
656
+ self.reset
657
+
658
+ # :TODO: Handle the additional_constraints
659
+
660
+ params ||= @fields
661
+ params = stringify_keys( params )
662
+ @fields = deep_copy( params )
663
+
664
+ # Use the constraints list to extract all the parameters that have corresponding
665
+ # constraints
666
+ self.constraints.each do |field, constraint|
667
+ self.log.debug " applying %s to any %p parameter/s" % [ constraint, field ]
668
+ value = params.delete( field.to_s )
669
+ self.log.debug " value is: %p" % [ value ]
670
+ self.apply_constraint( constraint, value )
671
+ end
672
+
673
+ # Any left over are unknown
674
+ params.keys.each do |field|
675
+ self.log.debug " unknown field %p" % [ field ]
676
+ @unknown << field
674
677
  end
678
+
679
+ @validated = true
675
680
  end
676
681
 
677
682
 
678
- ### Constrain a value to a parseable Date
679
- def match_date( val )
680
- return Date.parse( val ) rescue nil
683
+ ### Apply the specified +constraint+ (a Strelka::ParamValidator::Constraint object) to
684
+ ### the given +value+, and add the field to the appropriate field list based on the
685
+ ### result.
686
+ def apply_constraint( constraint, value )
687
+ if value
688
+ result = constraint.apply( value, self.untaint_all? )
689
+
690
+ if !result.nil?
691
+ self.log.debug " constraint for %p passed: %p" % [ constraint.name, result ]
692
+ self[ constraint.name ] = result
693
+ else
694
+ self.log.debug " constraint for %p failed" % [ constraint.name ]
695
+ @invalid[ constraint.name.to_s ] = value
696
+ end
697
+ elsif constraint.required?
698
+ self.log.debug " missing parameter for %p" % [ constraint.name ]
699
+ @missing << constraint.name.to_s
700
+ end
681
701
  end
682
702
 
683
703
 
684
- ### Constrain a value to alpha characters (a-z, case-insensitive)
685
- def match_alpha( val )
686
- return self.match_builtin_constraint( val, :alpha )
704
+ ### Clear existing validation information, merge the specified +params+ with any existing
705
+ ### raw fields, and re-run the validation.
706
+ def revalidate( params={} )
707
+ merged_fields = self.fields.merge( params )
708
+ self.reset
709
+ self.validate( merged_fields )
687
710
  end
688
711
 
689
712
 
690
- ### Constrain a value to alpha characters (a-z, case-insensitive and 0-9)
691
- def match_alphanumeric( val )
692
- return self.match_builtin_constraint( val, :alphanumeric )
713
+ ## Fetch the constraint/s that apply to the parameter named name as a Regexp, if possible.
714
+ def constraint_regexp_for( name )
715
+ self.log.debug " searching for a constraint for %p" % [ name ]
716
+
717
+ # Fetch the constraint's regexp
718
+ constraint = self.constraints[ name.to_sym ]
719
+ raise ScriptError,
720
+ "can't route on a parameter with a %p" % [ constraint.class ] unless
721
+ constraint.respond_to?( :pattern )
722
+
723
+ re = constraint.pattern
724
+ self.log.debug " bounded constraint is: %p" % [ re ]
725
+
726
+ # Unbind the pattern from beginning or end of line.
727
+ # :TODO: This is pretty ugly. Find a better way of modifying the regex.
728
+ re_str = re.to_s.
729
+ sub( %r{\(\?[\-mix]+:(.*)\)}, '\1' ).
730
+ gsub( PARAMETER_PATTERN_STRIP_RE, '' )
731
+ self.log.debug " stripped constraint pattern down to: %p" % [ re_str ]
732
+
733
+ return Regexp.new( "(?<#{name}>#{re_str})", re.options )
693
734
  end
694
735
 
695
736
 
696
- ### Constrain a value to any printable characters + whitespace, newline, and CR.
697
- def match_printable( val )
698
- return self.match_builtin_constraint( val, :printable )
737
+ ### Returns the valid fields after expanding Rails-style
738
+ ### 'customer[address][street]' variables into multi-level hashes.
739
+ def valid
740
+ self.validate unless self.validated?
741
+
742
+ unless @parsed_params
743
+ @parsed_params = {}
744
+ for key, value in @valid
745
+ value = [ value ] if key.to_s.end_with?( '[]' )
746
+ if key.to_s.include?( '[' )
747
+ build_deep_hash( value, @parsed_params, get_levels(key.to_s) )
748
+ else
749
+ @parsed_params[ key ] = value
750
+ end
751
+ end
752
+ end
753
+
754
+ return @parsed_params
699
755
  end
700
- alias_method :match_string, :match_printable
701
756
 
702
757
 
703
- ### Constrain a value to any UTF-8 word characters.
704
- def match_word( val )
705
- return self.match_builtin_constraint( val, :word )
758
+ ### Index fetch operator; fetch the validated (and possible parsed) value for
759
+ ### form field +key+.
760
+ def []( key )
761
+ return @valid[ key.to_sym ]
706
762
  end
707
763
 
708
764
 
709
- ### Override the parent class's definition to (not-sloppily) match email
710
- ### addresses.
711
- def match_email( val )
712
- return self.match_builtin_constraint( val, :email )
765
+ ### Index assignment operator; set the validated value for form field +key+
766
+ ### to the specified +val+.
767
+ def []=( key, val )
768
+ @parsed_params = nil
769
+ return @valid[ key.to_sym ] = val
713
770
  end
714
771
 
715
772
 
716
- ### Match valid hostnames according to the rules of the URL RFC.
717
- def match_hostname( val )
718
- return self.match_builtin_constraint( val, :hostname )
773
+ ### Returns +true+ if there were no arguments given.
774
+ def empty?
775
+ return self.fields.empty?
719
776
  end
720
777
 
721
778
 
722
- ### Match valid UUIDs.
723
- def match_uuid( val )
724
- return self.match_builtin_constraint( val, :uuid )
779
+ ### Returns +true+ if there were arguments given.
780
+ def args?
781
+ return !self.fields.empty?
725
782
  end
783
+ alias_method :has_args?, :args?
726
784
 
727
785
 
728
- ### Match valid URIs
729
- def match_uri( val )
730
- return self.match_builtin_constraint( val, :uri ) do |m|
731
- URI.parse( m.to_s )
732
- end
733
- rescue URI::InvalidURIError => err
734
- self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
735
- return nil
736
- rescue NoMethodError
737
- self.log.debug "Ignoring bug in URI#parse"
738
- return nil
786
+ ### The names of fields that were required, but missing from the parameter list.
787
+ def missing
788
+ self.validate unless self.validated?
789
+ return @missing
739
790
  end
740
791
 
741
792
 
742
- #
743
- # :section: Constraint method
744
- #
793
+ ### The Hash of fields that were present, but invalid (didn't match the field's constraint)
794
+ def invalid
795
+ self.validate unless self.validated?
796
+ return @invalid
797
+ end
745
798
 
746
- ### Apply one or more +constraints+ to the field value/s corresponding to
747
- ### +key+.
748
- def do_constraint( key, constraints )
749
- self.log.debug "Applying constraints %p to field %p" % [ constraints, key ]
750
- constraints.each do |constraint|
751
- case constraint
752
- when String
753
- apply_string_constraint( key, constraint )
754
- when Hash
755
- apply_hash_constraint( key, constraint )
756
- when Proc, Method
757
- apply_proc_constraint( key, constraint )
758
- when Regexp
759
- apply_regexp_constraint( key, constraint )
760
- else
761
- raise "unknown constraint type %p" % [constraint]
762
- end
763
- end
799
+
800
+ ### The names of fields that were present in the parameters, but didn't have a corresponding
801
+ ### constraint.
802
+ def unknown
803
+ self.validate unless self.validated?
804
+ return @unknown
764
805
  end
765
806
 
766
807
 
767
- ### Applies a builtin constraint to form[key].
768
- def apply_string_constraint( key, constraint )
769
- # FIXME: multiple elements
770
- rval = self.__send__( "match_#{constraint}", @form[key].to_s )
771
- self.log.debug "Tried a string constraint: %p: %p" %
772
- [ @form[key].to_s, rval ]
773
- self.set_form_value( key, rval, constraint )
808
+ ### Returns +true+ if any fields are missing or contain invalid values.
809
+ def errors?
810
+ return !self.okay?
774
811
  end
812
+ alias_method :has_errors?, :errors?
775
813
 
776
814
 
777
- ### Apply a constraint given as a Hash to the value/s corresponding to the
778
- ### specified +key+:
779
- ###
780
- ### constraint::
781
- ### A builtin constraint (as a Symbol; e.g., :email), a Regexp, or a Proc.
782
- ### name::
783
- ### A description of the constraint should it fail and be listed in #invalid.
784
- ### params::
785
- ### If +constraint+ is a Proc, this field should contain a list of other
786
- ### fields to send to the Proc.
787
- def apply_hash_constraint( key, constraint )
788
- action = constraint["constraint"]
789
-
790
- rval = case action
791
- when String
792
- self.apply_string_constraint( key, action )
793
- when Regexp
794
- self.apply_regexp_constraint( key, action )
795
- when Proc
796
- if args = constraint["params"]
797
- args.collect! {|field| @form[field] }
798
- self.apply_proc_constraint( key, action, *args )
799
- else
800
- self.apply_proc_constraint( key, action )
801
- end
802
- end
815
+ ### Return +true+ if all required fields were present and all present fields validated
816
+ ### correctly.
817
+ def okay?
818
+ return (self.missing.empty? && self.invalid.empty?)
819
+ end
803
820
 
804
- # If the validation failed, and there's a name for this constraint, replace
805
- # the name in @invalid_fields with the name
806
- if !rval && constraint["name"]
807
- @invalid_fields[ key ] = constraint["name"]
808
- end
809
821
 
810
- return rval
822
+ ### Return an array of field names which had some kind of error associated
823
+ ### with them.
824
+ def error_fields
825
+ return self.missing | self.invalid.keys
811
826
  end
812
827
 
813
828
 
814
- ### Apply a constraint that was specified as a Proc to the value for the given
815
- ### +key+
816
- def apply_proc_constraint( key, constraint, *params )
817
- value = nil
829
+ ### Return an error message for each missing or invalid field; if
830
+ ### +includeUnknown+ is +true+, also include messages for unknown fields.
831
+ def error_messages( include_unknown=false )
832
+ msgs = []
818
833
 
819
- unless params.empty?
820
- value = constraint.to_proc.call( *params )
821
- else
822
- value = constraint.to_proc.call( @form[key] )
823
- end
834
+ msgs += self.missing_param_errors + self.invalid_param_errors
835
+ msgs += self.unknown_param_errors if include_unknown
824
836
 
825
- self.set_form_value( key, value, constraint )
826
- rescue => err
827
- self.log.error "%p while validating %p using %p: %s (from %s)" %
828
- [ err.class, key, constraint, err.message, err.backtrace.first ]
829
- self.set_form_value( key, nil, constraint )
837
+ return msgs
830
838
  end
831
839
 
832
840
 
833
- ### Applies regexp constraint to form[key]
834
- def apply_regexp_constraint( key, constraint )
835
- self.log.debug "Validating %p via regexp %p" % [ @form[key], constraint ]
841
+ ### Return an Array of error messages, one for each field missing from the last validation.
842
+ def missing_param_errors
843
+ return self.missing.collect do |field|
844
+ constraint = self.constraints[ field.to_sym ] or
845
+ raise NameError, "no such field %p!" % [ field ]
846
+ "Missing value for '%s'" % [ constraint.description ]
847
+ end
848
+ end
836
849
 
837
- if match = constraint.match( @form[key].to_s )
838
- self.log.debug " matched %p" % [match[0]]
839
850
 
840
- if match.captures.empty?
841
- self.log.debug " no captures, using whole match: %p" % [match[0]]
842
- self.set_form_value( key, match[0], constraint )
843
- elsif match.names.length > 1
844
- self.log.debug " extracting hash of named captures: %p" % [ match.names ]
845
- hash = match.names.inject( {} ) do |accum,name|
846
- accum[ name.to_sym ] = match[ name ]
847
- accum
848
- end
851
+ ### Return an Array of error messages, one for each field that was invalid from the last
852
+ ### validation.
853
+ def invalid_param_errors
854
+ return self.invalid.collect do |field, _|
855
+ constraint = self.constraints[ field.to_sym ] or
856
+ raise NameError, "no such field %p!" % [ field ]
857
+ "Invalid value for '%s'" % [ constraint.description ]
858
+ end
859
+ end
849
860
 
850
- self.set_form_value( key, hash, constraint )
851
- elsif match.captures.length == 1
852
- self.log.debug " extracting one capture: %p" % [match.captures.first]
853
- self.set_form_value( key, match.captures.first, constraint )
854
- else
855
- self.log.debug " extracting multiple captures: %p" % [match.captures]
856
- self.set_form_value( key, match.captures, constraint )
857
- end
858
- else
859
- self.set_form_value( key, nil, constraint )
861
+
862
+ ### Return an Array of error messages, one for each field present in the parameters in the last
863
+ ### validation that didn't have a constraint associated with it.
864
+ def unknown_param_errors
865
+ self.log.debug "Fetching unknown param errors for %p." % [ self.unknown ]
866
+ return self.unknown.collect do |field|
867
+ "Unknown parameter '%s'" % [ field.capitalize ]
860
868
  end
861
869
  end
862
870
 
863
871
 
864
- ### Set the form value for the given +key+. If +value+ is false, add it to
865
- ### the list of invalid fields with a description derived from the specified
866
- ### +constraint+. Called by constraint methods when they succeed.
867
- def set_form_value( key, value, constraint )
868
- key.untaint
872
+ ### Return a new ParamValidator with the additional +params+ merged into
873
+ ### its values and re-validated.
874
+ def merge( params )
875
+ copy = self.dup
876
+ copy.merge!( params )
877
+ return copy
878
+ end
879
+
880
+
881
+ ### Merge the specified +params+ into the receiving ParamValidator and
882
+ ### re-validate the resulting values.
883
+ def merge!( params )
884
+ return if params.empty?
885
+ self.log.debug "Merging parameters for revalidation: %p" % [ params ]
886
+ self.revalidate( params )
887
+ end
869
888
 
870
- # Have to test for nil because valid values might be false.
871
- if !value.nil?
872
- self.log.debug "Setting form value for %p to %p (constraint was %p)" %
873
- [ key, value, constraint ]
874
- if self.untaint?( key )
875
- if value.respond_to?( :each_value )
876
- value.each_value( &:untaint )
877
- elsif value.is_a?( Array )
878
- value.each( &:untaint )
879
- else
880
- value.untaint
881
- end
882
- end
883
889
 
884
- @form[key] = value
885
- return true
890
+ ### Returns an array containing valid parameters in the validator corresponding to the
891
+ ### given +selector+(s).
892
+ def values_at( *selector )
893
+ selector.map!( &:to_sym )
894
+ return self.valid.values_at( *selector )
895
+ end
886
896
 
887
- else
888
- self.log.debug "Clearing form value for %p (constraint was %p)" %
889
- [ key, constraint ]
890
- @form.delete( key )
891
- @invalid_fields ||= {}
892
- @invalid_fields[ key ] ||= []
893
-
894
- unless @invalid_fields[ key ].include?( constraint )
895
- @invalid_fields[ key ].push( constraint )
896
- end
897
- return false
898
- end
899
- end
900
-
901
-
902
- ### Formvalidator hack:
903
- ### The formvalidator filters method has a bug where he assumes an array
904
- ### when it is in fact a string for multiple values (ie anytime you have a
905
- ### text-area with newlines in it).
906
- # def filters
907
- # @filters_array = Array(@profile[:filters]) unless(@filters_array)
908
- # @filters_array.each do |filter|
909
- #
910
- # if respond_to?( "filter_#{filter}" )
911
- # @form.keys.each do |field|
912
- # # If a key has multiple elements, apply filter to each element
913
- # @field_array = Array( @form[field] )
914
- #
915
- # if @field_array.length > 1
916
- # @field_array.each_index do |i|
917
- # elem = @field_array[i]
918
- # @field_array[i] = self.send("filter_#{filter}", elem)
919
- # end
920
- # else
921
- # if not @form[field].to_s.empty?
922
- # @form[field] = self.send("filter_#{filter}", @form[field].to_s)
923
- # end
924
- # end
925
- # end
926
- # end
927
- # end
928
- # @form
929
- # end
930
897
 
931
898
 
932
899
  #######
933
900
  private
934
901
  #######
935
902
 
936
- ### Overridden to eliminate use of default #to_a (deprecated)
937
- def strify_array( array )
938
- array = [ array ] if !array.is_a?( Array )
939
- array.map do |m|
940
- m = (Array === m) ? strify_array(m) : m
941
- m = (Hash === m) ? strify_hash(m) : m
942
- Symbol === m ? m.to_s : m
943
- end
944
- end
945
-
946
-
947
903
  ### Build a deep hash out of the given parameter +value+
948
904
  def build_deep_hash( value, hash, levels )
949
905
  if levels.length == 0