strelka 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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