strelka 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|