strelka 0.0.1pre4 → 0.0.1.pre129
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/History.rdoc +1 -1
- data/IDEAS.rdoc +62 -0
- data/Manifest.txt +38 -7
- data/README.rdoc +124 -5
- data/Rakefile +22 -6
- data/bin/leash +102 -157
- data/contrib/hoetemplate/.autotest.erb +23 -0
- data/contrib/hoetemplate/History.rdoc.erb +4 -0
- data/contrib/hoetemplate/Manifest.txt.erb +8 -0
- data/contrib/hoetemplate/README.rdoc.erb +17 -0
- data/contrib/hoetemplate/Rakefile.erb +24 -0
- data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
- data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
- data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
- data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
- data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
- data/data/strelka/apps/hello-world +30 -0
- data/lib/strelka/app/defaultrouter.rb +49 -30
- data/lib/strelka/app/errors.rb +121 -0
- data/lib/strelka/app/exclusiverouter.rb +40 -0
- data/lib/strelka/app/filters.rb +18 -7
- data/lib/strelka/app/negotiation.rb +122 -0
- data/lib/strelka/app/parameters.rb +171 -14
- data/lib/strelka/app/paramvalidator.rb +751 -0
- data/lib/strelka/app/plugins.rb +66 -46
- data/lib/strelka/app/restresources.rb +499 -0
- data/lib/strelka/app/router.rb +73 -0
- data/lib/strelka/app/routing.rb +140 -18
- data/lib/strelka/app/templating.rb +12 -3
- data/lib/strelka/app.rb +174 -24
- data/lib/strelka/constants.rb +0 -20
- data/lib/strelka/exceptions.rb +29 -0
- data/lib/strelka/httprequest/acceptparams.rb +377 -0
- data/lib/strelka/httprequest/negotiation.rb +257 -0
- data/lib/strelka/httprequest.rb +155 -7
- data/lib/strelka/httpresponse/negotiation.rb +579 -0
- data/lib/strelka/httpresponse.rb +140 -0
- data/lib/strelka/logging.rb +4 -1
- data/lib/strelka/mixins.rb +53 -0
- data/lib/strelka.rb +22 -1
- data/spec/data/error.tmpl +1 -0
- data/spec/lib/constants.rb +0 -1
- data/spec/lib/helpers.rb +21 -0
- data/spec/strelka/app/defaultrouter_spec.rb +41 -35
- data/spec/strelka/app/errors_spec.rb +212 -0
- data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
- data/spec/strelka/app/filters_spec.rb +196 -0
- data/spec/strelka/app/negotiation_spec.rb +73 -0
- data/spec/strelka/app/parameters_spec.rb +149 -0
- data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
- data/spec/strelka/app/plugins_spec.rb +26 -19
- data/spec/strelka/app/restresources_spec.rb +393 -0
- data/spec/strelka/app/router_spec.rb +63 -0
- data/spec/strelka/app/routing_spec.rb +183 -9
- data/spec/strelka/app/templating_spec.rb +1 -2
- data/spec/strelka/app_spec.rb +265 -32
- data/spec/strelka/exceptions_spec.rb +53 -0
- data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
- data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
- data/spec/strelka/httprequest_spec.rb +204 -14
- data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
- data/spec/strelka/httpresponse_spec.rb +114 -0
- data/spec/strelka/mixins_spec.rb +99 -0
- data.tar.gz.sig +1 -0
- metadata +175 -79
- metadata.gz.sig +2 -0
- data/IDEAS.textile +0 -174
- data/data/strelka/apps/strelka-admin +0 -65
- data/data/strelka/apps/strelka-setup +0 -26
- data/data/strelka/bootstrap-config.rb +0 -34
- data/data/strelka/templates/admin/console.tmpl +0 -21
- data/data/strelka/templates/layout.tmpl +0 -30
- data/lib/strelka/process.rb +0 -19
@@ -0,0 +1,751 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
require 'forwardable'
|
6
|
+
require 'date'
|
7
|
+
require 'formvalidator'
|
8
|
+
|
9
|
+
require 'strelka/mixins'
|
10
|
+
require 'strelka' unless defined?( Strelka )
|
11
|
+
require 'strelka/app' unless defined?( Strelka::App )
|
12
|
+
|
13
|
+
|
14
|
+
# A validator for user parameters.
|
15
|
+
#
|
16
|
+
# == Usage
|
17
|
+
#
|
18
|
+
# require 'strelka/app/formvalidator'
|
19
|
+
#
|
20
|
+
# # Profile specifies validation criteria for input
|
21
|
+
# profile = {
|
22
|
+
# :required => :name,
|
23
|
+
# :optional => [:email, :description],
|
24
|
+
# :filters => [:strip, :squeeze],
|
25
|
+
# :untaint_all_constraints => true,
|
26
|
+
# :descriptions => {
|
27
|
+
# :email => "Customer Email",
|
28
|
+
# :description => "Issue Description",
|
29
|
+
# :name => "Customer Name",
|
30
|
+
# },
|
31
|
+
# :constraints => {
|
32
|
+
# :email => :email,
|
33
|
+
# :name => /^[\x20-\x7f]+$/,
|
34
|
+
# :description => /^[\x20-\x7f]+$/,
|
35
|
+
# },
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# # Create a validator object and pass in a hash of request parameters and the
|
39
|
+
# # profile hash.
|
40
|
+
# validator = Strelka::App::ParamValidator.new
|
41
|
+
# validator.validate( req_params, profile )
|
42
|
+
#
|
43
|
+
# # Now if there weren't any errors, send the success page
|
44
|
+
# if validator.okay?
|
45
|
+
# return success_template
|
46
|
+
#
|
47
|
+
# # Otherwise fill in the error template with auto-generated error messages
|
48
|
+
# # and return that instead.
|
49
|
+
# else
|
50
|
+
# failure_template.errors( validator.error_messages )
|
51
|
+
# return failure_template
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
class Strelka::App::ParamValidator < ::FormValidator
|
55
|
+
extend Forwardable
|
56
|
+
include Strelka::Loggable
|
57
|
+
|
58
|
+
|
59
|
+
# Validation default config
|
60
|
+
DEFAULT_PROFILE = {
|
61
|
+
:descriptions => {},
|
62
|
+
}
|
63
|
+
|
64
|
+
#
|
65
|
+
# RFC822 Email Address Regex
|
66
|
+
# --------------------------
|
67
|
+
#
|
68
|
+
# Originally written by Cal Henderson
|
69
|
+
# c.f. http://iamcal.com/publish/articles/php/parsing_email/
|
70
|
+
#
|
71
|
+
# Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
|
72
|
+
#
|
73
|
+
# Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
|
74
|
+
# http://creativecommons.org/licenses/by-sa/2.5/
|
75
|
+
#
|
76
|
+
RFC822_EMAIL_ADDRESS = begin
|
77
|
+
qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
|
78
|
+
dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
|
79
|
+
atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
|
80
|
+
'\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
|
81
|
+
quoted_pair = '\\x5c[\\x00-\\x7f]'
|
82
|
+
domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
|
83
|
+
quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
|
84
|
+
domain_ref = atom
|
85
|
+
sub_domain = "(?:#{domain_ref}|#{domain_literal})"
|
86
|
+
word = "(?:#{atom}|#{quoted_string})"
|
87
|
+
domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
|
88
|
+
local_part = "#{word}(?:\\x2e#{word})*"
|
89
|
+
addr_spec = "#{local_part}\\x40#{domain}"
|
90
|
+
/\A#{addr_spec}\z/n
|
91
|
+
end
|
92
|
+
|
93
|
+
# Pattern for (loosely) matching a valid hostname. This isn't strictly RFC-compliant
|
94
|
+
# because, in practice, many hostnames used on the Internet aren't.
|
95
|
+
RFC1738_HOSTNAME = begin
|
96
|
+
alphadigit = /[a-z0-9]/i
|
97
|
+
# toplabel = alpha | alpha *[ alphadigit | "-" ] alphadigit
|
98
|
+
toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
|
99
|
+
# domainlabel = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
|
100
|
+
domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
|
101
|
+
# hostname = *[ domainlabel "." ] toplabel
|
102
|
+
hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
|
103
|
+
end
|
104
|
+
|
105
|
+
# Pattern for countint the number of hash levels in a parameter key
|
106
|
+
PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
|
107
|
+
|
108
|
+
# The Hash of builtin constraints that are validated against a regular
|
109
|
+
# expression.
|
110
|
+
BUILTIN_CONSTRAINT_PATTERNS = {
|
111
|
+
:boolean => /^(?<boolean>t(?:rue)?|y(?:es)?|[10]|no?|f(?:alse)?)$/i,
|
112
|
+
:integer => /^(?<integer>[\-\+]?\d+)$/,
|
113
|
+
:float => /^(?<float>[\-\+]?(?:\d*\.\d+|\d+)(?:e[\-\+]?\d+)?)$/i,
|
114
|
+
:alpha => /^(?<alpha>[[:alpha:]]+)$/,
|
115
|
+
:alphanumeric => /^(?<alphanumeric>[[:alnum:]]+)$/,
|
116
|
+
:printable => /\A(?<printable>[[:print:][:blank:]\r\n]+)\z/,
|
117
|
+
:string => /\A(?<string>[[:print:][:blank:]\r\n]+)\z/,
|
118
|
+
:word => /^(?<word>[[:word:]]+)$/,
|
119
|
+
:email => /^(?<email>#{RFC822_EMAIL_ADDRESS})$/,
|
120
|
+
:hostname => /^(?<hostname>#{RFC1738_HOSTNAME})$/,
|
121
|
+
:uri => /^(?<uri>#{URI::URI_REF})$/,
|
122
|
+
}
|
123
|
+
|
124
|
+
|
125
|
+
### Return a Regex for the built-in constraint associated with the given +name+. If
|
126
|
+
### the builtin constraint is not pattern-based, or there is no such constraint,
|
127
|
+
### returns +nil+.
|
128
|
+
def self::pattern_for_constraint( name )
|
129
|
+
return BUILTIN_CONSTRAINT_PATTERNS[ name.to_sym ]
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
#################################################################
|
134
|
+
### I N S T A N C E M E T H O D S
|
135
|
+
#################################################################
|
136
|
+
|
137
|
+
### Create a new Strelka::App::ParamValidator object.
|
138
|
+
def initialize( profile, params=nil )
|
139
|
+
@form = {}
|
140
|
+
@raw_form = {}
|
141
|
+
@profile = DEFAULT_PROFILE.merge( profile )
|
142
|
+
@invalid_fields = []
|
143
|
+
@missing_fields = []
|
144
|
+
@unknown_fields = []
|
145
|
+
@required_fields = []
|
146
|
+
@require_some_fields = []
|
147
|
+
@optional_fields = []
|
148
|
+
@filters_array = []
|
149
|
+
@untaint_fields = []
|
150
|
+
|
151
|
+
@parsed_params = nil
|
152
|
+
|
153
|
+
self.validate( params ) if params
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
### Copy constructor.
|
158
|
+
def initialize_copy( original )
|
159
|
+
@form = @form.clone
|
160
|
+
@raw_form = @form.clone
|
161
|
+
@profile = @profile.clone
|
162
|
+
@invalid_fields = @invalid_fields.clone
|
163
|
+
@missing_fields = @missing_fields.clone
|
164
|
+
@unknown_fields = @unknown_fields.clone
|
165
|
+
@required_fields = @required_fields.clone
|
166
|
+
@require_some_fields = @require_some_fields.clone
|
167
|
+
@optional_fields = @optional_fields.clone
|
168
|
+
@filters_array = @filters_array.clone
|
169
|
+
@untaint_fields = @untaint_fields.clone
|
170
|
+
|
171
|
+
@parsed_params = @parsed_params.clone if @parsed_params
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
######
|
177
|
+
public
|
178
|
+
######
|
179
|
+
|
180
|
+
# The raw form data Hash
|
181
|
+
attr_reader :raw_form
|
182
|
+
|
183
|
+
# The validated form data Hash
|
184
|
+
attr_reader :form
|
185
|
+
|
186
|
+
|
187
|
+
### Stringified description of the validator
|
188
|
+
def to_s
|
189
|
+
"%d parameters (%d valid, %d invalid, %d missing)" % [
|
190
|
+
self.raw_form.size,
|
191
|
+
self.form.size,
|
192
|
+
self.invalid.size,
|
193
|
+
self.missing.size,
|
194
|
+
]
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
### Hash of field descriptions
|
199
|
+
def descriptions
|
200
|
+
return @profile[:descriptions]
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
### Set hash of field descriptions
|
205
|
+
def descriptions=( new_descs )
|
206
|
+
return @profile[:descriptions] = new_descs
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
### Validate the input in +params+. If the optional +additional_profile+ is
|
211
|
+
### given, merge it with the validator's default profile before validating.
|
212
|
+
def validate( params, additional_profile=nil )
|
213
|
+
@raw_form = params.dup
|
214
|
+
profile = @profile
|
215
|
+
|
216
|
+
if additional_profile
|
217
|
+
self.log.debug "Merging additional profile %p" % [additional_profile]
|
218
|
+
profile = @profile.merge( additional_profile )
|
219
|
+
end
|
220
|
+
|
221
|
+
super( params, profile )
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
### Overridden to remove the check for extra keys.
|
226
|
+
def check_profile_syntax( profile )
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
### Index operator; fetch the validated value for form field +key+.
|
231
|
+
def []( key )
|
232
|
+
return @form[ key.to_s ]
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
### Index assignment operator; set the validated value for form field +key+
|
237
|
+
### to the specified +val+.
|
238
|
+
def []=( key, val )
|
239
|
+
return @form[ key.to_s ] = val
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
### Returns +true+ if there were no arguments given.
|
244
|
+
def empty?
|
245
|
+
return @form.empty?
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
### Returns +true+ if there were arguments given.
|
250
|
+
def args?
|
251
|
+
return !@form.empty?
|
252
|
+
end
|
253
|
+
alias_method :has_args?, :args?
|
254
|
+
|
255
|
+
|
256
|
+
### Returns +true+ if any fields are missing or contain invalid values.
|
257
|
+
def errors?
|
258
|
+
return !self.okay?
|
259
|
+
end
|
260
|
+
alias_method :has_errors?, :errors?
|
261
|
+
|
262
|
+
|
263
|
+
### Return +true+ if all required fields were present and validated
|
264
|
+
### correctly.
|
265
|
+
def okay?
|
266
|
+
return (self.missing.empty? && self.invalid.empty?)
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
### Returns +true+ if the given +field+ is one that should be untainted.
|
271
|
+
def untaint?( field )
|
272
|
+
self.log.debug "Checking to see if %p should be untainted." % [field]
|
273
|
+
rval = ( @untaint_all ||
|
274
|
+
@untaint_fields.include?(field) ||
|
275
|
+
@untaint_fields.include?(field.to_sym) )
|
276
|
+
|
277
|
+
if rval
|
278
|
+
self.log.debug " ...yep it should."
|
279
|
+
else
|
280
|
+
self.log.debug " ...nope; untaint fields is: %p" % [ @untaint_fields ]
|
281
|
+
end
|
282
|
+
|
283
|
+
return rval
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
### Overridden so that the presence of :untaint_constraint_fields doesn't
|
288
|
+
### disable the :untaint_all_constraints flag.
|
289
|
+
def untaint_all_constraints
|
290
|
+
@untaint_all = @profile[:untaint_all_constraints] ? true : false
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
|
295
|
+
### Return an array of field names which had some kind of error associated
|
296
|
+
### with them.
|
297
|
+
def error_fields
|
298
|
+
return self.missing | self.invalid.keys
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
### Get the description for the specified field.
|
303
|
+
def get_description( field )
|
304
|
+
return @profile[:descriptions][ field.to_s ] if
|
305
|
+
@profile[:descriptions].key?( field.to_s )
|
306
|
+
|
307
|
+
desc = field.to_s.
|
308
|
+
gsub( /.*\[(\w+)\]/, "\\1" ).
|
309
|
+
gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
|
310
|
+
gsub( /^(.)/ ) {|m| m.upcase }
|
311
|
+
return desc
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
### Return an error message for each missing or invalid field; if
|
316
|
+
### +includeUnknown+ is +true+, also include messages for unknown fields.
|
317
|
+
def error_messages( include_unknown=false )
|
318
|
+
self.log.debug "Building error messages from descriptions: %p" %
|
319
|
+
[ @profile[:descriptions] ]
|
320
|
+
msgs = []
|
321
|
+
self.missing.each do |field|
|
322
|
+
msgs << "Missing value for '%s'" % self.get_description( field )
|
323
|
+
end
|
324
|
+
|
325
|
+
self.invalid.each do |field, constraint|
|
326
|
+
msgs << "Invalid value for '%s'" % self.get_description( field )
|
327
|
+
end
|
328
|
+
|
329
|
+
if include_unknown
|
330
|
+
self.unknown.each do |field|
|
331
|
+
msgs << "Unknown parameter '%s'" % self.get_description( field )
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
return msgs
|
336
|
+
end
|
337
|
+
|
338
|
+
|
339
|
+
### Returns a distinct list of missing fields. Overridden to eliminate the
|
340
|
+
### "undefined method `<=>' for :foo:Symbol" error.
|
341
|
+
def missing
|
342
|
+
@missing_fields.uniq.sort_by {|f| f.to_s}
|
343
|
+
end
|
344
|
+
|
345
|
+
### Returns a distinct list of unknown fields.
|
346
|
+
def unknown
|
347
|
+
(@unknown_fields - @invalid_fields.keys).uniq.sort_by {|f| f.to_s}
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
### Returns the valid fields after expanding Rails-style
|
352
|
+
### 'customer[address][street]' variables into multi-level hashes.
|
353
|
+
def valid
|
354
|
+
if @parsed_params.nil?
|
355
|
+
@parsed_params = {}
|
356
|
+
valid = super()
|
357
|
+
|
358
|
+
for key, value in valid
|
359
|
+
value = [ value ] if key.end_with?( '[]' )
|
360
|
+
if key.include?( '[' )
|
361
|
+
build_deep_hash( value, @parsed_params, get_levels(key) )
|
362
|
+
else
|
363
|
+
@parsed_params[ key ] = value
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
return @parsed_params
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
### Return a new ParamValidator with the additional +params+ merged into
|
373
|
+
### its values and re-validated.
|
374
|
+
def merge( params )
|
375
|
+
copy = self.dup
|
376
|
+
copy.merge!( params )
|
377
|
+
return copy
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
### Merge the specified +params+ into the receiving ParamValidator and
|
382
|
+
### re-validate the resulting values.
|
383
|
+
def merge!( params )
|
384
|
+
return if params.empty?
|
385
|
+
|
386
|
+
self.log.debug "Merging parameters for revalidation: %p" % [ params ]
|
387
|
+
@missing_fields.clear
|
388
|
+
@unknown_fields.clear
|
389
|
+
@required_fields.clear
|
390
|
+
@invalid_fields.clear
|
391
|
+
@untaint_fields.clear
|
392
|
+
@require_some_fields.clear
|
393
|
+
@optional_fields.clear
|
394
|
+
@form.clear
|
395
|
+
|
396
|
+
newparams = @raw_form.merge( params )
|
397
|
+
@raw_form.clear
|
398
|
+
|
399
|
+
self.log.debug " merged raw form is: %p" % [ newparams ]
|
400
|
+
self.validate( newparams )
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
### Returns an array containing valid parameters in the validator corresponding to the
|
405
|
+
### given +selector+(s).
|
406
|
+
def values_at( *selector )
|
407
|
+
selector.map!( &:to_s )
|
408
|
+
return @form.values_at( *selector )
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
#
|
413
|
+
# :section: Constraint methods
|
414
|
+
#
|
415
|
+
|
416
|
+
### Try to match the specified +val+ using the built-in constraint pattern
|
417
|
+
### associated with +name+, returning the matched value upon success, and +nil+
|
418
|
+
### if the +val+ didn't match. If a +block+ is given, it's called with the
|
419
|
+
### associated MatchData on success, and its return value is returned instead of
|
420
|
+
### the matching String.
|
421
|
+
def match_builtin_constraint( val, name )
|
422
|
+
self.log.debug "Validating %p using built-in constraint %p" % [ val, name ]
|
423
|
+
re = self.class.pattern_for_constraint( name.to_sym )
|
424
|
+
match = re.match( val ) or return nil
|
425
|
+
self.log.debug " matched: %p" % [ match ]
|
426
|
+
|
427
|
+
if block_given?
|
428
|
+
begin
|
429
|
+
return yield( match )
|
430
|
+
rescue ArgumentError
|
431
|
+
return nil
|
432
|
+
end
|
433
|
+
else
|
434
|
+
return match.to_s
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
|
439
|
+
### Constrain a value to +true+ (or +yes+) and +false+ (or +no+).
|
440
|
+
def match_boolean( val )
|
441
|
+
return self.match_builtin_constraint( val, :boolean ) do |m|
|
442
|
+
m.to_s.start_with?( 'y', 't', '1' )
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
|
447
|
+
### Constrain a value to an integer
|
448
|
+
def match_integer( val )
|
449
|
+
return self.match_builtin_constraint( val, :integer ) do |m|
|
450
|
+
Integer( m.to_s )
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
### Contrain a value to a Float
|
456
|
+
def match_float( val )
|
457
|
+
return self.match_builtin_constraint( val, :float ) do |m|
|
458
|
+
Float( m.to_s )
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
|
463
|
+
### Constrain a value to a parseable Date
|
464
|
+
def match_date( val )
|
465
|
+
return Date.parse( val ) rescue nil
|
466
|
+
end
|
467
|
+
|
468
|
+
|
469
|
+
### Constrain a value to alpha characters (a-z, case-insensitive)
|
470
|
+
def match_alpha( val )
|
471
|
+
return self.match_builtin_constraint( val, :alpha )
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
### Constrain a value to alpha characters (a-z, case-insensitive and 0-9)
|
476
|
+
def match_alphanumeric( val )
|
477
|
+
return self.match_builtin_constraint( val, :alphanumeric )
|
478
|
+
end
|
479
|
+
|
480
|
+
|
481
|
+
### Constrain a value to any printable characters + whitespace, newline, and CR.
|
482
|
+
def match_printable( val )
|
483
|
+
return self.match_builtin_constraint( val, :printable )
|
484
|
+
end
|
485
|
+
alias_method :match_string, :match_printable
|
486
|
+
|
487
|
+
|
488
|
+
### Constrain a value to any UTF-8 word characters.
|
489
|
+
def match_word( val )
|
490
|
+
return self.match_builtin_constraint( val, :word )
|
491
|
+
end
|
492
|
+
|
493
|
+
|
494
|
+
### Override the parent class's definition to (not-sloppily) match email
|
495
|
+
### addresses.
|
496
|
+
def match_email( val )
|
497
|
+
return self.match_builtin_constraint( val, :email )
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
### Match valid hostnames according to the rules of the URL RFC.
|
502
|
+
def match_hostname( val )
|
503
|
+
return self.match_builtin_constraint( val, :hostname )
|
504
|
+
end
|
505
|
+
|
506
|
+
|
507
|
+
### Match valid URIs
|
508
|
+
def match_uri( val )
|
509
|
+
return self.match_builtin_constraint( val, :uri ) do |m|
|
510
|
+
URI.parse( m.to_s )
|
511
|
+
end
|
512
|
+
rescue URI::InvalidURIError => err
|
513
|
+
self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
|
514
|
+
return nil
|
515
|
+
rescue NoMethodError
|
516
|
+
self.log.debug "Ignoring bug in URI#parse"
|
517
|
+
return nil
|
518
|
+
end
|
519
|
+
|
520
|
+
|
521
|
+
### Apply one or more +constraints+ to the field value/s corresponding to
|
522
|
+
### +key+.
|
523
|
+
def do_constraint( key, constraints )
|
524
|
+
constraints.each do |constraint|
|
525
|
+
case constraint
|
526
|
+
when String
|
527
|
+
apply_string_constraint( key, constraint )
|
528
|
+
when Hash
|
529
|
+
apply_hash_constraint( key, constraint )
|
530
|
+
when Proc, Method
|
531
|
+
apply_proc_constraint( key, constraint )
|
532
|
+
when Regexp
|
533
|
+
apply_regexp_constraint( key, constraint )
|
534
|
+
else
|
535
|
+
raise "unknown constraint type %p" % [constraint]
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
|
541
|
+
### Applies a builtin constraint to form[key].
|
542
|
+
def apply_string_constraint( key, constraint )
|
543
|
+
# FIXME: multiple elements
|
544
|
+
rval = self.__send__( "match_#{constraint}", @form[key].to_s )
|
545
|
+
self.log.debug "Tried a string constraint: %p: %p" %
|
546
|
+
[ @form[key].to_s, rval ]
|
547
|
+
self.set_form_value( key, rval, constraint )
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
### Apply a constraint given as a Hash to the value/s corresponding to the
|
552
|
+
### specified +key+:
|
553
|
+
###
|
554
|
+
### constraint::
|
555
|
+
### A builtin constraint (as a Symbol; e.g., :email), a Regexp, or a Proc.
|
556
|
+
### name::
|
557
|
+
### A description of the constraint should it fail and be listed in #invalid.
|
558
|
+
### params::
|
559
|
+
### If +constraint+ is a Proc, this field should contain a list of other
|
560
|
+
### fields to send to the Proc.
|
561
|
+
def apply_hash_constraint( key, constraint )
|
562
|
+
action = constraint["constraint"]
|
563
|
+
|
564
|
+
rval = case action
|
565
|
+
when String
|
566
|
+
self.apply_string_constraint( key, action )
|
567
|
+
when Regexp
|
568
|
+
self.apply_regexp_constraint( key, action )
|
569
|
+
when Proc
|
570
|
+
if args = constraint["params"]
|
571
|
+
args.collect! {|field| @form[field] }
|
572
|
+
self.apply_proc_constraint( key, action, *args )
|
573
|
+
else
|
574
|
+
self.apply_proc_constraint( key, action )
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
# If the validation failed, and there's a name for this constraint, replace
|
579
|
+
# the name in @invalid_fields with the name
|
580
|
+
if !rval && constraint["name"]
|
581
|
+
@invalid_fields[ key ] = constraint["name"]
|
582
|
+
end
|
583
|
+
|
584
|
+
return rval
|
585
|
+
end
|
586
|
+
|
587
|
+
|
588
|
+
### Apply a constraint that was specified as a Proc to the value for the given
|
589
|
+
### +key+
|
590
|
+
def apply_proc_constraint( key, constraint, *params )
|
591
|
+
value = nil
|
592
|
+
|
593
|
+
unless params.empty?
|
594
|
+
value = constraint.to_proc.call( *params )
|
595
|
+
else
|
596
|
+
value = constraint.to_proc.call( @form[key] )
|
597
|
+
end
|
598
|
+
|
599
|
+
self.set_form_value( key, value, constraint )
|
600
|
+
rescue => err
|
601
|
+
self.log.error "%p while validating %p using %p: %s (from %s)" %
|
602
|
+
[ err.class, key, constraint, err.message, err.backtrace.first ]
|
603
|
+
self.set_form_value( key, nil, constraint )
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
### Applies regexp constraint to form[key]
|
608
|
+
def apply_regexp_constraint( key, constraint )
|
609
|
+
self.log.debug "Validating %p via regexp %p" % [ @form[key], constraint ]
|
610
|
+
|
611
|
+
if match = constraint.match( @form[key].to_s )
|
612
|
+
self.log.debug " matched %p" % [match[0]]
|
613
|
+
|
614
|
+
if match.captures.empty?
|
615
|
+
self.log.debug " no captures, using whole match: %p" % [match[0]]
|
616
|
+
self.set_form_value( key, match[0], constraint )
|
617
|
+
elsif match.names.length > 1
|
618
|
+
self.log.debug " extracting hash of named captures: %p" % [ match.names ]
|
619
|
+
hash = match.names.inject( {} ) do |accum,name|
|
620
|
+
accum[ name.to_sym ] = match[ name ]
|
621
|
+
accum
|
622
|
+
end
|
623
|
+
|
624
|
+
self.set_form_value( key, hash, constraint )
|
625
|
+
elsif match.captures.length == 1
|
626
|
+
self.log.debug " extracting one capture: %p" % [match.captures.first]
|
627
|
+
self.set_form_value( key, match.captures.first, constraint )
|
628
|
+
else
|
629
|
+
self.log.debug " extracting multiple captures: %p" % [match.captures]
|
630
|
+
self.set_form_value( key, match.captures, constraint )
|
631
|
+
end
|
632
|
+
else
|
633
|
+
self.set_form_value( key, nil, constraint )
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
|
638
|
+
### Set the form value for the given +key+. If +value+ is false, add it to
|
639
|
+
### the list of invalid fields with a description derived from the specified
|
640
|
+
### +constraint+.
|
641
|
+
def set_form_value( key, value, constraint )
|
642
|
+
key.untaint
|
643
|
+
|
644
|
+
# Have to test for nil because valid values might be false.
|
645
|
+
if !value.nil?
|
646
|
+
self.log.debug "Setting form value for %p to %p (constraint was %p)" %
|
647
|
+
[ key, value, constraint ]
|
648
|
+
if self.untaint?( key )
|
649
|
+
if value.respond_to?( :each_value )
|
650
|
+
value.each_value( &:untaint )
|
651
|
+
elsif value.is_a?( Array )
|
652
|
+
value.each( &:untaint )
|
653
|
+
else
|
654
|
+
value.untaint
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
@form[key] = value
|
659
|
+
return true
|
660
|
+
|
661
|
+
else
|
662
|
+
self.log.debug "Clearing form value for %p (constraint was %p)" %
|
663
|
+
[ key, constraint ]
|
664
|
+
@form.delete( key )
|
665
|
+
@invalid_fields ||= {}
|
666
|
+
@invalid_fields[ key ] ||= []
|
667
|
+
|
668
|
+
unless @invalid_fields[ key ].include?( constraint )
|
669
|
+
@invalid_fields[ key ].push( constraint )
|
670
|
+
end
|
671
|
+
return false
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
|
676
|
+
### Formvalidator hack:
|
677
|
+
### The formvalidator filters method has a bug where he assumes an array
|
678
|
+
### when it is in fact a string for multiple values (ie anytime you have a
|
679
|
+
### text-area with newlines in it).
|
680
|
+
def filters
|
681
|
+
@filters_array = Array(@profile[:filters]) unless(@filters_array)
|
682
|
+
@filters_array.each do |filter|
|
683
|
+
|
684
|
+
if respond_to?( "filter_#{filter}" )
|
685
|
+
@form.keys.each do |field|
|
686
|
+
# If a key has multiple elements, apply filter to each element
|
687
|
+
@field_array = Array( @form[field] )
|
688
|
+
|
689
|
+
if @field_array.length > 1
|
690
|
+
@field_array.each_index do |i|
|
691
|
+
elem = @field_array[i]
|
692
|
+
@field_array[i] = self.send("filter_#{filter}", elem)
|
693
|
+
end
|
694
|
+
else
|
695
|
+
if not @form[field].to_s.empty?
|
696
|
+
@form[field] = self.send("filter_#{filter}", @form[field].to_s)
|
697
|
+
end
|
698
|
+
end
|
699
|
+
end
|
700
|
+
end
|
701
|
+
end
|
702
|
+
@form
|
703
|
+
end
|
704
|
+
|
705
|
+
|
706
|
+
#######
|
707
|
+
private
|
708
|
+
#######
|
709
|
+
|
710
|
+
### Overridden to eliminate use of default #to_a (deprecated)
|
711
|
+
def strify_array( array )
|
712
|
+
array = [ array ] if !array.is_a?( Array )
|
713
|
+
array.map do |m|
|
714
|
+
m = (Array === m) ? strify_array(m) : m
|
715
|
+
m = (Hash === m) ? strify_hash(m) : m
|
716
|
+
Symbol === m ? m.to_s : m
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
|
721
|
+
### Build a deep hash out of the given parameter +value+
|
722
|
+
def build_deep_hash( value, hash, levels )
|
723
|
+
if levels.length == 0
|
724
|
+
value.untaint
|
725
|
+
elsif hash.nil?
|
726
|
+
{ levels.first => build_deep_hash(value, nil, levels[1..-1]) }
|
727
|
+
else
|
728
|
+
hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) })
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
|
733
|
+
### Get the number of hash levels in the specified +key+
|
734
|
+
### Stolen from the CGIMethods class in Rails' action_controller.
|
735
|
+
def get_levels( key )
|
736
|
+
all, main, bracketed, trailing = PARAMS_HASH_RE.match( key ).to_a
|
737
|
+
if main.nil?
|
738
|
+
return []
|
739
|
+
elsif trailing
|
740
|
+
return [key.untaint]
|
741
|
+
elsif bracketed
|
742
|
+
return [main.untaint] + bracketed.slice(1...-1).split('][').collect {|k| k.untaint }
|
743
|
+
else
|
744
|
+
return [main.untaint]
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
end # class Strelka::App::ParamValidator
|
749
|
+
|
750
|
+
|
751
|
+
|