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.
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
+