strelka 0.0.1pre4 → 0.0.1.pre129

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/History.rdoc +1 -1
  2. data/IDEAS.rdoc +62 -0
  3. data/Manifest.txt +38 -7
  4. data/README.rdoc +124 -5
  5. data/Rakefile +22 -6
  6. data/bin/leash +102 -157
  7. data/contrib/hoetemplate/.autotest.erb +23 -0
  8. data/contrib/hoetemplate/History.rdoc.erb +4 -0
  9. data/contrib/hoetemplate/Manifest.txt.erb +8 -0
  10. data/contrib/hoetemplate/README.rdoc.erb +17 -0
  11. data/contrib/hoetemplate/Rakefile.erb +24 -0
  12. data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
  13. data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
  14. data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
  15. data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
  16. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
  17. data/data/strelka/apps/hello-world +30 -0
  18. data/lib/strelka/app/defaultrouter.rb +49 -30
  19. data/lib/strelka/app/errors.rb +121 -0
  20. data/lib/strelka/app/exclusiverouter.rb +40 -0
  21. data/lib/strelka/app/filters.rb +18 -7
  22. data/lib/strelka/app/negotiation.rb +122 -0
  23. data/lib/strelka/app/parameters.rb +171 -14
  24. data/lib/strelka/app/paramvalidator.rb +751 -0
  25. data/lib/strelka/app/plugins.rb +66 -46
  26. data/lib/strelka/app/restresources.rb +499 -0
  27. data/lib/strelka/app/router.rb +73 -0
  28. data/lib/strelka/app/routing.rb +140 -18
  29. data/lib/strelka/app/templating.rb +12 -3
  30. data/lib/strelka/app.rb +174 -24
  31. data/lib/strelka/constants.rb +0 -20
  32. data/lib/strelka/exceptions.rb +29 -0
  33. data/lib/strelka/httprequest/acceptparams.rb +377 -0
  34. data/lib/strelka/httprequest/negotiation.rb +257 -0
  35. data/lib/strelka/httprequest.rb +155 -7
  36. data/lib/strelka/httpresponse/negotiation.rb +579 -0
  37. data/lib/strelka/httpresponse.rb +140 -0
  38. data/lib/strelka/logging.rb +4 -1
  39. data/lib/strelka/mixins.rb +53 -0
  40. data/lib/strelka.rb +22 -1
  41. data/spec/data/error.tmpl +1 -0
  42. data/spec/lib/constants.rb +0 -1
  43. data/spec/lib/helpers.rb +21 -0
  44. data/spec/strelka/app/defaultrouter_spec.rb +41 -35
  45. data/spec/strelka/app/errors_spec.rb +212 -0
  46. data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
  47. data/spec/strelka/app/filters_spec.rb +196 -0
  48. data/spec/strelka/app/negotiation_spec.rb +73 -0
  49. data/spec/strelka/app/parameters_spec.rb +149 -0
  50. data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
  51. data/spec/strelka/app/plugins_spec.rb +26 -19
  52. data/spec/strelka/app/restresources_spec.rb +393 -0
  53. data/spec/strelka/app/router_spec.rb +63 -0
  54. data/spec/strelka/app/routing_spec.rb +183 -9
  55. data/spec/strelka/app/templating_spec.rb +1 -2
  56. data/spec/strelka/app_spec.rb +265 -32
  57. data/spec/strelka/exceptions_spec.rb +53 -0
  58. data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
  59. data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
  60. data/spec/strelka/httprequest_spec.rb +204 -14
  61. data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
  62. data/spec/strelka/httpresponse_spec.rb +114 -0
  63. data/spec/strelka/mixins_spec.rb +99 -0
  64. data.tar.gz.sig +1 -0
  65. metadata +175 -79
  66. metadata.gz.sig +2 -0
  67. data/IDEAS.textile +0 -174
  68. data/data/strelka/apps/strelka-admin +0 -65
  69. data/data/strelka/apps/strelka-setup +0 -26
  70. data/data/strelka/bootstrap-config.rb +0 -34
  71. data/data/strelka/templates/admin/console.tmpl +0 -21
  72. data/data/strelka/templates/layout.tmpl +0 -30
  73. 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
+