strelka 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +36 -2
- data/History.rdoc +9 -0
- data/bin/strelka +1 -0
- data/lib/strelka/app/parameters.rb +2 -0
- data/lib/strelka/app/restresources.rb +1 -1
- data/lib/strelka/httprequest.rb +5 -2
- data/lib/strelka/mixins.rb +54 -0
- data/lib/strelka/paramvalidator.rb +645 -689
- data/lib/strelka/plugins.rb +7 -2
- data/lib/strelka.rb +2 -2
- data/spec/strelka/app_spec.rb +4 -2
- data/spec/strelka/httprequest_spec.rb +33 -0
- data/spec/strelka/paramvalidator_spec.rb +7 -3
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
@@ -19,9 +19,9 @@ require 'strelka/app' unless defined?( Strelka::App )
|
|
19
19
|
#
|
20
20
|
# == Usage
|
21
21
|
#
|
22
|
-
#
|
22
|
+
# require 'strelka/paramvalidator'
|
23
23
|
#
|
24
|
-
#
|
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
|
-
#
|
33
|
-
#
|
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
|
-
#
|
39
|
+
# # success page template
|
40
40
|
# if validator.okay?
|
41
41
|
# tmpl = template :success
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
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
|
-
#
|
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
|
-
|
58
|
+
Loggability,
|
59
|
+
Strelka::MethodUtilities
|
60
|
+
include Strelka::DataUtilities
|
59
61
|
|
60
|
-
# Loggability API --
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
148
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
197
|
-
|
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
|
-
|
203
|
-
######
|
113
|
+
subtype = TYPES[ spec.class ] or
|
114
|
+
raise "No constraint type for a %p validation spec" % [ spec.class ]
|
204
115
|
|
205
|
-
|
206
|
-
|
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
|
-
|
212
|
-
|
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
|
-
|
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
|
-
|
224
|
-
|
225
|
-
|
226
|
-
end
|
135
|
+
######
|
136
|
+
public
|
137
|
+
######
|
227
138
|
|
139
|
+
# The name of the field the constraint governs
|
140
|
+
attr_reader :name
|
228
141
|
|
229
|
-
|
230
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
244
|
-
constraint
|
245
|
-
|
246
|
-
|
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
|
-
|
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
|
270
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
-
|
307
|
-
|
308
|
-
|
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
|
-
|
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
|
-
|
318
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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
|
-
|
354
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
407
|
-
|
408
|
-
|
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
|
-
|
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
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
-
|
456
|
-
|
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
|
-
|
463
|
-
|
464
|
-
|
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
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
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
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
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
|
-
|
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
|
-
###
|
545
|
-
|
546
|
-
|
547
|
-
|
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
|
-
|
551
|
-
|
552
|
-
|
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
|
-
###
|
557
|
-
###
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
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
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
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
|
-
|
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
|
-
###
|
578
|
-
###
|
579
|
-
def
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
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
|
-
|
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
|
-
###
|
596
|
-
|
597
|
-
|
598
|
-
|
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
|
-
|
611
|
-
|
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
|
-
###
|
616
|
-
|
617
|
-
|
618
|
-
|
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
|
-
###
|
655
|
-
def
|
656
|
-
return self.
|
657
|
-
|
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
|
-
###
|
663
|
-
def
|
664
|
-
|
665
|
-
|
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
|
-
###
|
671
|
-
def
|
672
|
-
|
673
|
-
|
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
|
-
###
|
679
|
-
|
680
|
-
|
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
|
-
###
|
685
|
-
|
686
|
-
|
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
|
-
|
691
|
-
def
|
692
|
-
|
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
|
-
###
|
697
|
-
|
698
|
-
|
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
|
-
###
|
704
|
-
|
705
|
-
|
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
|
-
###
|
710
|
-
###
|
711
|
-
def
|
712
|
-
|
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
|
-
###
|
717
|
-
def
|
718
|
-
return self.
|
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
|
-
###
|
723
|
-
def
|
724
|
-
return self.
|
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
|
-
###
|
729
|
-
def
|
730
|
-
|
731
|
-
|
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
|
-
|
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
|
-
|
747
|
-
###
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
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
|
-
###
|
768
|
-
def
|
769
|
-
|
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
|
-
###
|
778
|
-
###
|
779
|
-
|
780
|
-
|
781
|
-
|
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
|
-
|
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
|
-
###
|
815
|
-
### +
|
816
|
-
def
|
817
|
-
|
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
|
-
|
820
|
-
|
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
|
-
|
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
|
-
###
|
834
|
-
def
|
835
|
-
self.
|
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
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
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
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
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
|
-
###
|
865
|
-
###
|
866
|
-
|
867
|
-
|
868
|
-
|
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
|
-
|
885
|
-
|
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
|