PageRenderTime 64ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/strelka/paramvalidator.rb

https://bitbucket.org/ged/strelka
Ruby | 1006 lines | 574 code | 210 blank | 222 comment | 48 complexity | bff31937c0d89cebf9ad0f9a0450a92a MD5 | raw file
  1. # -*- ruby -*-
  2. # vim: set nosta noet ts=4 sw=4:
  3. # encoding: utf-8
  4. require 'uri'
  5. require 'forwardable'
  6. require 'date'
  7. require 'loggability'
  8. require 'strelka/mixins'
  9. require 'strelka' unless defined?( Strelka )
  10. require 'strelka/app' unless defined?( Strelka::App )
  11. # A validator for user parameters.
  12. #
  13. # == Usage
  14. #
  15. # require 'strelka/paramvalidator'
  16. #
  17. # validator = Strelka::ParamValidator.new
  18. #
  19. # # Add validation criteria for input parameters
  20. # validator.add( :name, /^(?<lastname>\S+), (?<firstname>\S+)$/, "Customer Name" )
  21. # validator.add( :email, "Customer Email" )
  22. # validator.add( :feedback, :printable, "Customer Feedback" )
  23. # validator.override( :email, :printable, "Your Email Address" )
  24. #
  25. # # Untaint all parameter values which match their constraints
  26. # validate.untaint_all_constraints = true
  27. #
  28. # # Now pass in tainted values in a hash (e.g., from an HTML form)
  29. # validator.validate( req.params )
  30. #
  31. # # Now if there weren't any errors, use some form values to fill out the
  32. # # success page template
  33. # if validator.okay?
  34. # tmpl = template :success
  35. # tmpl.firstname = validator[:name][:firstname]
  36. # tmpl.lastname = validator[:name][:lastname]
  37. # tmpl.email = validator[:email]
  38. # tmpl.feedback = validator[:feedback]
  39. # return tmpl
  40. #
  41. # # Otherwise fill in the error template with auto-generated error messages
  42. # # and return that instead.
  43. # else
  44. # tmpl = template :feedback_form
  45. # tmpl.errors = validator.error_messages
  46. # return tmpl
  47. # end
  48. #
  49. class Strelka::ParamValidator
  50. extend Forwardable,
  51. Loggability,
  52. Strelka::MethodUtilities
  53. include Strelka::DataUtilities
  54. # Loggability API -- log to the 'strelka' logger
  55. log_to :strelka
  56. # Pattern for countint the number of hash levels in a parameter key
  57. PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
  58. # Pattern to use to strip binding operators from parameter patterns so they
  59. # can be used in the middle of routing Regexps.
  60. PARAMETER_PATTERN_STRIP_RE = Regexp.union( '^', '$', '\\A', '\\z', '\\Z' )
  61. # The base constraint type.
  62. class Constraint
  63. extend Loggability,
  64. Strelka::MethodUtilities
  65. # Loggability API -- log to the 'strelka' logger
  66. log_to :strelka
  67. # Flags that are passed as Symbols when declaring a parameter
  68. FLAGS = [ :required, :untaint, :multiple ]
  69. # Map of constraint specification types to their equivalent Constraint class.
  70. TYPES = { Proc => self }
  71. ### Register the given +subclass+ as the Constraint class to be used when
  72. ### the specified +syntax_class+ is given as the constraint in a parameter
  73. ### declaration.
  74. def self::register_type( syntax_class )
  75. self.log.debug "Registering %p as the constraint class for %p objects" %
  76. [ self, syntax_class ]
  77. TYPES[ syntax_class ] = self
  78. end
  79. ### Return a Constraint object appropriate for the given +field+ and +spec+.
  80. def self::for( field, spec=nil, *options, &block )
  81. self.log.debug "Building Constraint for %p (%p)" % [ field, spec ]
  82. # Handle omitted constraint
  83. if spec.is_a?( String ) || FLAGS.include?( spec )
  84. options.unshift( spec )
  85. spec = nil
  86. end
  87. spec ||= block
  88. subtype = TYPES[ spec.class ] or
  89. raise "No constraint type for a %p validation spec" % [ spec.class ]
  90. return subtype.new( field, spec, *options, &block )
  91. end
  92. ### Create a new Constraint for the field with the given +name+, configuring it with the
  93. ### specified +args+. The +block+ is what does the actual validation, at least in the
  94. ### base class.
  95. def initialize( name, *args, &block )
  96. @name = name
  97. @block = block
  98. @description = args.shift if args.first.is_a?( String )
  99. @required = args.include?( :required )
  100. @untaint = args.include?( :untaint )
  101. @multiple = args.include?( :multiple )
  102. end
  103. ######
  104. public
  105. ######
  106. # The name of the field the constraint governs
  107. attr_reader :name
  108. # The constraint's check block
  109. attr_reader :block
  110. # The field's description
  111. attr_writer :description
  112. ##
  113. # Returns true if the field can have multiple values.
  114. attr_predicate :multiple?
  115. ##
  116. # Returns true if the field associated with the constraint is required in
  117. # order for the parameters to be valid.
  118. attr_predicate :required?
  119. ##
  120. # Returns true if the constraint will also untaint its result before returning it.
  121. attr_predicate :untaint?
  122. ### Check the given value against the constraint and return the result if it passes.
  123. def apply( value, force_untaint=false )
  124. untaint = self.untaint? || force_untaint
  125. if self.multiple?
  126. return self.check_multiple( value, untaint )
  127. else
  128. return self.check( value, untaint )
  129. end
  130. end
  131. ### Comparison operator Constraints are equal if theyre for the same field,
  132. ### theyre of the same type, and their blocks are the same.
  133. def ==( other )
  134. return self.name == other.name &&
  135. other.instance_of?( self.class ) &&
  136. self.block == other.block
  137. end
  138. ### Get the description of the field.
  139. def description
  140. return @description || self.generate_description
  141. end
  142. ### Return the constraint expressed as a String.
  143. def to_s
  144. desc = self.validator_description
  145. flags = []
  146. flags << 'required' if self.required?
  147. flags << 'multiple' if self.multiple?
  148. flags << 'untaint' if self.untaint?
  149. desc << " (%s)" % [ flags.join(',') ] unless flags.empty?
  150. return desc
  151. end
  152. #########
  153. protected
  154. #########
  155. ### Return a description of the validation provided by the constraint object.
  156. def validator_description
  157. desc = 'a custom validator'
  158. if self.block
  159. location = self.block.source_location
  160. desc << " on line %d of %s" % [ location[1], location[0] ]
  161. end
  162. return desc
  163. end
  164. ### Check the specified value against the constraint and return the results. By
  165. ### default, this just calls to_proc and the block and calls the result with the
  166. ### value as its argument.
  167. def check( value, untaint )
  168. return self.block.to_proc.call( value ) if self.block
  169. value.untaint if untaint && value.respond_to?( :untaint )
  170. return value
  171. end
  172. ### Check the given +values+ against the constraint and return the results if
  173. ### all of them succeed.
  174. def check_multiple( values, untaint )
  175. values = [ values ] unless values.is_a?( Array )
  176. results = []
  177. values.each do |value|
  178. result = self.check( value, untaint ) or return nil
  179. results << result
  180. end
  181. return results
  182. end
  183. ### Generate a description from the name of the field.
  184. def generate_description
  185. self.log.debug "Auto-generating description for %p" % [ self ]
  186. desc = self.name.to_s.
  187. gsub( /.*\[(\w+)\]/, "\\1" ).
  188. gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
  189. gsub( /^(.)/ ) {|m| m.upcase }
  190. self.log.debug " generated: %p" % [ desc ]
  191. return desc
  192. end
  193. end # class Constraint
  194. # A constraint expressed as a regular expression.
  195. class RegexpConstraint < Constraint
  196. # Use this for constraints expressed as Regular Expressions
  197. register_type Regexp
  198. ### Create a new RegexpConstraint that will validate the field of the given
  199. ### +name+ with the specified +pattern+.
  200. def initialize( name, pattern, *args, &block )
  201. @pattern = pattern
  202. super( name, *args, &block )
  203. end
  204. ######
  205. public
  206. ######
  207. # The constraint's pattern
  208. attr_reader :pattern
  209. ### Check the +value+ against the regular expression and return its
  210. ### match groups if successful.
  211. def check( value, untaint )
  212. self.log.debug "Validating %p via regexp %p" % [ value, self.pattern ]
  213. match = self.pattern.match( value.to_s ) or return nil
  214. if match.captures.empty?
  215. self.log.debug " no captures, using whole match: %p" % [match[0]]
  216. return super( match[0], untaint )
  217. elsif match.names.length > 1
  218. self.log.debug " extracting hash of named captures: %p" % [ match.names ]
  219. rhash = self.matched_hash( match, untaint )
  220. return super( rhash, untaint )
  221. elsif match.captures.length == 1
  222. self.log.debug " extracting one capture: %p" % [match.captures.first]
  223. return super( match.captures.first, untaint )
  224. else
  225. self.log.debug " extracting multiple captures: %p" % [match.captures]
  226. values = match.captures
  227. values.map {|val| val.untaint if val } if untaint
  228. return super( values, untaint )
  229. end
  230. end
  231. ### Return a Hash of the given +match+ object's named captures, untainting the values
  232. ### if +untaint+ is true.
  233. def matched_hash( match, untaint )
  234. return match.names.inject( {} ) do |accum,name|
  235. value = match[ name ]
  236. value.untaint if untaint && value
  237. accum[ name.to_sym ] = value
  238. accum
  239. end
  240. end
  241. ### Return the constraint expressed as a String.
  242. def validator_description
  243. return "a value matching the pattern %p" % [ self.pattern ]
  244. end
  245. end # class RegexpConstraint
  246. # A constraint class that uses a collection of predefined patterns.
  247. class BuiltinConstraint < RegexpConstraint
  248. # Use this for constraints expressed as Symbols or who are missing a constraint spec (nil)
  249. register_type Symbol
  250. register_type NilClass
  251. #
  252. # RFC822 Email Address Regex
  253. # --------------------------
  254. #
  255. # Originally written by Cal Henderson
  256. # c.f. http://iamcal.com/publish/articles/php/parsing_email/
  257. #
  258. # Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
  259. #
  260. # Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
  261. # http://creativecommons.org/licenses/by-sa/2.5/
  262. #
  263. RFC822_EMAIL_ADDRESS = begin
  264. qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
  265. dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
  266. atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
  267. '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
  268. quoted_pair = '\\x5c[\\x00-\\x7f]'
  269. domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
  270. quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
  271. domain_ref = atom
  272. sub_domain = "(?:#{domain_ref}|#{domain_literal})"
  273. word = "(?:#{atom}|#{quoted_string})"
  274. domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
  275. local_part = "#{word}(?:\\x2e#{word})*"
  276. addr_spec = "#{local_part}\\x40#{domain}"
  277. /\A#{addr_spec}\z/n
  278. end
  279. # Pattern for (loosely) matching a valid hostname. This isn't strictly RFC-compliant
  280. # because, in practice, many hostnames used on the Internet aren't.
  281. RFC1738_HOSTNAME = begin
  282. alphadigit = /[a-z0-9]/i
  283. # toplabel = alpha | alpha *[ alphadigit | "-" ] alphadigit
  284. toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
  285. # domainlabel = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
  286. domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
  287. # hostname = *[ domainlabel "." ] toplabel
  288. hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
  289. end
  290. # Validation regexp for JSON
  291. # Converted to oniguruma syntax from the PCRE example at:
  292. # http://stackoverflow.com/questions/2583472/regex-to-validate-json
  293. JSON_VALIDATOR_RE = begin
  294. pair = ''
  295. json = /^
  296. (?<json> \s* (?:
  297. # number
  298. (?: 0 | -? [1-9]\d* (?:\.\d+)? (?:[eE][+-]?\d+)? )
  299. |
  300. # boolean
  301. (?: true | false | null )
  302. |
  303. # string
  304. "(?: [^"\\[:cntrl:]]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*"
  305. |
  306. # array
  307. \[ (?: \g<json> (?: , \g<json> )* )? \s* \]
  308. |
  309. # object
  310. \{
  311. (?:
  312. # first pair
  313. \s* "(?: [^"\\]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*" \s* : \g<json>
  314. # following pairs
  315. (?: , \s* "(?: [^"\\]* | \\["\\bfnrt\/] | \\u\p{XDigit}{4} )*" \s* : \g<json> )*
  316. )?
  317. \s*
  318. \}
  319. ) \s* )
  320. \z/ux
  321. end
  322. # The Hash of builtin constraints that are validated against a regular
  323. # expression.
  324. # :TODO: Document that these are the built-in constraints that can be used in a route
  325. BUILTIN_CONSTRAINT_PATTERNS = {
  326. :boolean => /^(?<boolean>t(?:rue)?|y(?:es)?|[10]|no?|f(?:alse)?)$/i,
  327. :integer => /^(?<integer>[\-\+]?\d+)$/,
  328. :float => /^(?<float>[\-\+]?(?:\d*\.\d+|\d+)(?:e[\-\+]?\d+)?)$/i,
  329. :alpha => /^(?<alpha>[[:alpha:]]+)$/,
  330. :alphanumeric => /^(?<alphanumeric>[[:alnum:]]+)$/,
  331. :printable => /\A(?<printable>[[:print:][:blank:]\r\n]+)\z/,
  332. :string => /\A(?<string>[[:print:][:blank:]\r\n]+)\z/,
  333. :word => /^(?<word>[[:word:]]+)$/,
  334. :email => /^(?<email>#{RFC822_EMAIL_ADDRESS})$/,
  335. :hostname => /^(?<hostname>#{RFC1738_HOSTNAME})$/,
  336. :uri => /^(?<uri>#{URI::URI_REF})$/,
  337. :uuid => /^(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})$/i,
  338. :date => /.*\d.*/,
  339. :datetime => /.*\d.*/,
  340. :json => JSON_VALIDATOR_RE,
  341. :md5sum => /^(?<md5sum>[[:xdigit:]]{32})$/i,
  342. :sha1sum => /^(?<sha1sum>[[:xdigit:]]{40})$/i,
  343. :sha256sum => /^(?<sha256sum>[[:xdigit:]]{64})$/i,
  344. :sha384sum => /^(?<sha384sum>[[:xdigit:]]{96})$/i,
  345. :sha512sum => /^(?<sha512sum>[[:xdigit:]]{128})$/i,
  346. }
  347. # Field values which result in a valid true value for :boolean constraints
  348. TRUE_VALUES = %w[t true y yes 1]
  349. #
  350. # Class methods
  351. #
  352. ##
  353. # Hash of named constraint patterns
  354. singleton_attr_reader :constraint_patterns
  355. @constraint_patterns = BUILTIN_CONSTRAINT_PATTERNS.dup
  356. ### Return true if name is the name of a built-in constraint.
  357. def self::valid?( name )
  358. return BUILTIN_CONSTRAINT_PATTERNS.key?( name.to_sym )
  359. end
  360. ### Reset the named patterns to the defaults. Mostly used for testing.
  361. def self::reset_constraint_patterns
  362. @constraint_patterns.replace( BUILTIN_CONSTRAINT_PATTERNS )
  363. end
  364. #
  365. # Instance methods
  366. #
  367. ### Create a new BuiltinConstraint using the pattern named name for the specified field.
  368. def initialize( field, name, *options, &block )
  369. name ||= field
  370. @pattern_name = name
  371. pattern = BUILTIN_CONSTRAINT_PATTERNS[ name.to_sym ] or
  372. raise ScriptError, "no such builtin constraint %p" % [ name ]
  373. super( field, pattern, *options, &block )
  374. end
  375. ######
  376. public
  377. ######
  378. # The name of the builtin pattern the field should be constrained by
  379. attr_reader :pattern_name
  380. ### Check for an additional post-processor method, and if it exists, return it as
  381. ### a Method object.
  382. def block
  383. if custom_block = super
  384. return custom_block
  385. else
  386. post_processor = "post_process_%s" % [ @pattern_name ]
  387. return nil unless self.respond_to?( post_processor, true )
  388. return self.method( post_processor )
  389. end
  390. end
  391. ### Return the constraint expressed as a String.
  392. def validator_description
  393. return "a '%s'" % [ self.pattern_name ]
  394. end
  395. #########
  396. protected
  397. #########
  398. ### Post-process a :boolean value.
  399. def post_process_boolean( val )
  400. return TRUE_VALUES.include?( val.to_s.downcase )
  401. end
  402. ### Constrain a value to a parseable Date
  403. def post_process_date( val )
  404. return Date.parse( val )
  405. rescue ArgumentError
  406. return nil
  407. end
  408. ### Constrain a value to a parseable Date
  409. def post_process_datetime( val )
  410. return Time.parse( val )
  411. rescue ArgumentError
  412. return nil
  413. end
  414. ### Constrain a value to a Float
  415. def post_process_float( val )
  416. return Float( val.to_s )
  417. end
  418. ### Post-process a valid :integer field.
  419. def post_process_integer( val )
  420. return Integer( val.to_s )
  421. end
  422. ### Post-process a valid :uri field.
  423. def post_process_uri( val )
  424. return URI.parse( val.to_s )
  425. rescue URI::InvalidURIError => err
  426. self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
  427. return nil
  428. rescue NoMethodError
  429. self.log.debug "Ignoring bug in URI#parse"
  430. return nil
  431. end
  432. end # class BuiltinConstraint
  433. #################################################################
  434. ### I N S T A N C E M E T H O D S
  435. #################################################################
  436. ### Create a new Strelka::ParamValidator object.
  437. def initialize
  438. @constraints = {}
  439. @fields = {}
  440. @untaint_all = false
  441. self.reset
  442. end
  443. ### Copy constructor.
  444. def initialize_copy( original )
  445. fields = deep_copy( original.fields )
  446. self.reset
  447. @fields = fields
  448. @constraints = deep_copy( original.constraints )
  449. end
  450. ######
  451. public
  452. ######
  453. # The constraints hash
  454. attr_reader :constraints
  455. # The Hash of raw field data (if validation has occurred)
  456. attr_reader :fields
  457. ##
  458. # Global untainting flag
  459. attr_predicate_accessor :untaint_all?
  460. alias_method :untaint_all_constraints=, :untaint_all=
  461. alias_method :untaint_all_constraints?, :untaint_all?
  462. ##
  463. # Returns +true+ if the paramvalidator has been given parameters to validate. Adding or
  464. # overriding constraints resets this.
  465. attr_predicate_accessor :validated?
  466. ### Reset the validation state.
  467. def reset
  468. self.log.debug "Resetting validation state."
  469. @validated = false
  470. @valid = {}
  471. @parsed_params = nil
  472. @missing = []
  473. @unknown = []
  474. @invalid = {}
  475. end
  476. ### :call-seq:
  477. ### add( name, *flags )
  478. ### add( name, constraint, *flags )
  479. ### add( name, description, *flags )
  480. ### add( name, constraint, description, *flags )
  481. ###
  482. ### Add a validation for a parameter with the specified +name+. The +args+ can include
  483. ### a constraint, a description, and one or more flags.
  484. def add( name, *args, &block )
  485. name = name.to_sym
  486. constraint = Constraint.for( name, *args, &block )
  487. # No-op if there's already a parameter with the same name and constraint
  488. if self.constraints.key?( name )
  489. return if self.constraints[ name ] == constraint
  490. raise ArgumentError,
  491. "parameter %p is already defined as %s; perhaps you meant to use #override?" %
  492. [ name.to_s, self.constraints[name] ]
  493. end
  494. self.log.debug "Adding parameter %p: %p" % [ name, constraint ]
  495. self.constraints[ name ] = constraint
  496. self.validated = false
  497. end
  498. ### Replace the existing parameter with the specified name. The args replace the
  499. ### existing description, constraints, and flags. See #add for details.
  500. def override( name, *args, &block )
  501. name = name.to_sym
  502. raise ArgumentError,
  503. "no parameter %p defined; perhaps you meant to use #add?" % [ name.to_s ] unless
  504. self.constraints.key?( name )
  505. self.log.debug "Overriding parameter %p" % [ name ]
  506. self.constraints[ name ] = Constraint.for( name, *args, &block )
  507. self.validated = false
  508. end
  509. ### Return the Array of parameter names the validator knows how to validate (as Strings).
  510. def param_names
  511. return self.constraints.keys.map( &:to_s ).sort
  512. end
  513. ### Stringified description of the validator
  514. def to_s
  515. "%d parameters (%d valid, %d invalid, %d missing)" % [
  516. self.fields.size,
  517. self.valid.size,
  518. self.invalid.size,
  519. self.missing.size,
  520. ]
  521. end
  522. ### Return a human-readable representation of the validator, suitable for debugging.
  523. def inspect
  524. required, optional = self.constraints.partition do |_, constraint|
  525. constraint.required?
  526. end
  527. return "#<%p:0x%016x %s, profile: [required: %s, optional: %s] global untaint: %s>" % [
  528. self.class,
  529. self.object_id / 2,
  530. self.to_s,
  531. required.empty? ? "(none)" : required.map( &:last ).map( &:name ).join(','),
  532. optional.empty? ? "(none)" : optional.map( &:last ).map( &:name ).join(','),
  533. self.untaint_all? ? "enabled" : "disabled",
  534. ]
  535. end
  536. ### Hash of field descriptions
  537. def descriptions
  538. return self.constraints.each_with_object({}) do |(field,constraint), hash|
  539. hash[ field ] = constraint.description
  540. end
  541. end
  542. ### Set field descriptions en masse to new_descs.
  543. def descriptions=( new_descs )
  544. new_descs.each do |name, description|
  545. raise NameError, "no parameter named #{name}" unless
  546. self.constraints.key?( name.to_sym )
  547. self.constraints[ name.to_sym ].description = description
  548. end
  549. end
  550. ### Get the description for the specified +field+.
  551. def get_description( field )
  552. constraint = self.constraints[ field.to_sym ] or return nil
  553. return constraint.description
  554. end
  555. ### Validate the input in +params+. If the optional +additional_constraints+ is
  556. ### given, merge it with the validator's existing constraints before validating.
  557. def validate( params=nil, additional_constraints=nil )
  558. self.log.debug "Validating."
  559. self.reset
  560. # :TODO: Handle the additional_constraints
  561. params ||= @fields
  562. params = stringify_keys( params )
  563. @fields = deep_copy( params )
  564. self.log.debug "Starting validation with fields: %p" % [ @fields ]
  565. # Use the constraints list to extract all the parameters that have corresponding
  566. # constraints
  567. self.constraints.each do |field, constraint|
  568. self.log.debug " applying %s to any %p parameter/s" % [ constraint, field ]
  569. value = params.delete( field.to_s )
  570. self.log.debug " value is: %p" % [ value ]
  571. self.apply_constraint( constraint, value )
  572. end
  573. # Any left over are unknown
  574. params.keys.each do |field|
  575. self.log.debug " unknown field %p" % [ field ]
  576. @unknown << field
  577. end
  578. @validated = true
  579. end
  580. ### Apply the specified +constraint+ (a Strelka::ParamValidator::Constraint object) to
  581. ### the given +value+, and add the field to the appropriate field list based on the
  582. ### result.
  583. def apply_constraint( constraint, value )
  584. if !( value.nil? || value == '' )
  585. result = constraint.apply( value, self.untaint_all? )
  586. if !result.nil?
  587. self.log.debug " constraint for %p passed: %p" % [ constraint.name, result ]
  588. self[ constraint.name ] = result
  589. else
  590. self.log.debug " constraint for %p failed" % [ constraint.name ]
  591. @invalid[ constraint.name.to_s ] = value
  592. end
  593. elsif constraint.required?
  594. self.log.debug " missing parameter for %p" % [ constraint.name ]
  595. @missing << constraint.name.to_s
  596. end
  597. end
  598. ### Clear existing validation information, merge the specified +params+ with any existing
  599. ### raw fields, and re-run the validation.
  600. def revalidate( params={} )
  601. merged_fields = self.fields.merge( params )
  602. self.reset
  603. self.validate( merged_fields )
  604. end
  605. ## Fetch the constraint/s that apply to the parameter named +name+ as a Regexp, if possible.
  606. def constraint_regexp_for( name )
  607. self.log.debug " searching for a constraint for %p" % [ name ]
  608. # Fetch the constraint's regexp
  609. constraint = self.constraints[ name.to_sym ] or
  610. raise NameError, "no such parameter %p" % [ name ]
  611. raise ScriptError,
  612. "can't route on a parameter with a %p" % [ constraint.class ] unless
  613. constraint.respond_to?( :pattern )
  614. re = constraint.pattern
  615. self.log.debug " bounded constraint is: %p" % [ re ]
  616. # Unbind the pattern from beginning or end of line.
  617. # :TODO: This is pretty ugly. Find a better way of modifying the regex.
  618. re_str = re.to_s.
  619. sub( %r{\(\?[\-mix]+:(.*)\)}, '\1' ).
  620. gsub( PARAMETER_PATTERN_STRIP_RE, '' )
  621. self.log.debug " stripped constraint pattern down to: %p" % [ re_str ]
  622. return Regexp.new( "(?<#{name}>#{re_str})", re.options )
  623. end
  624. ### Returns the valid fields after expanding Rails-style
  625. ### 'customer[address][street]' variables into multi-level hashes.
  626. def valid
  627. self.validate unless self.validated?
  628. self.log.debug "Building valid fields hash from raw data: %p" % [ @valid ]
  629. unless @parsed_params
  630. @parsed_params = {}
  631. for key, value in @valid
  632. self.log.debug " adding %s: %p" % [ key, value ]
  633. value = [ value ] if key.to_s.end_with?( '[]' )
  634. if key.to_s.include?( '[' )
  635. build_deep_hash( value, @parsed_params, get_levels(key.to_s) )
  636. else
  637. @parsed_params[ key ] = value
  638. end
  639. end
  640. end
  641. return @parsed_params
  642. end
  643. ### Index fetch operator; fetch the validated (and possible parsed) value for
  644. ### form field +key+.
  645. def []( key )
  646. self.validate unless self.validated?
  647. return @valid[ key.to_sym ]
  648. end
  649. ### Index assignment operator; set the validated value for form field +key+
  650. ### to the specified +val+.
  651. def []=( key, val )
  652. @parsed_params = nil
  653. @valid[ key.to_sym ] = val
  654. end
  655. ### Returns +true+ if there were no arguments given.
  656. def empty?
  657. return self.fields.empty?
  658. end
  659. ### Returns +true+ if there were arguments given.
  660. def args?
  661. return !self.fields.empty?
  662. end
  663. alias_method :has_args?, :args?
  664. ### The names of fields that were required, but missing from the parameter list.
  665. def missing
  666. self.validate unless self.validated?
  667. return @missing
  668. end
  669. ### The Hash of fields that were present, but invalid (didn't match the field's constraint)
  670. def invalid
  671. self.validate unless self.validated?
  672. return @invalid
  673. end
  674. ### The names of fields that were present in the parameters, but didn't have a corresponding
  675. ### constraint.
  676. def unknown
  677. self.validate unless self.validated?
  678. return @unknown
  679. end
  680. ### Returns +true+ if any fields are missing or contain invalid values.
  681. def errors?
  682. return !self.okay?
  683. end
  684. alias_method :has_errors?, :errors?
  685. ### Return +true+ if all required fields were present and all present fields validated
  686. ### correctly.
  687. def okay?
  688. return (self.missing.empty? && self.invalid.empty?)
  689. end
  690. ### Return an array of field names which had some kind of error associated
  691. ### with them.
  692. def error_fields
  693. return self.missing | self.invalid.keys
  694. end
  695. ### Return an error message for each missing or invalid field; if
  696. ### +includeUnknown+ is +true+, also include messages for unknown fields.
  697. def error_messages( include_unknown=false )
  698. msgs = []
  699. msgs += self.missing_param_errors + self.invalid_param_errors
  700. msgs += self.unknown_param_errors if include_unknown
  701. return msgs
  702. end
  703. ### Return an Array of error messages, one for each field missing from the last validation.
  704. def missing_param_errors
  705. return self.missing.collect do |field|
  706. constraint = self.constraints[ field.to_sym ] or
  707. raise NameError, "no such field %p!" % [ field ]
  708. "Missing value for '%s'" % [ constraint.description ]
  709. end
  710. end
  711. ### Return an Array of error messages, one for each field that was invalid from the last
  712. ### validation.
  713. def invalid_param_errors
  714. return self.invalid.collect do |field, _|
  715. constraint = self.constraints[ field.to_sym ] or
  716. raise NameError, "no such field %p!" % [ field ]
  717. "Invalid value for '%s'" % [ constraint.description ]
  718. end
  719. end
  720. ### Return an Array of error messages, one for each field present in the parameters in the last
  721. ### validation that didn't have a constraint associated with it.
  722. def unknown_param_errors
  723. self.log.debug "Fetching unknown param errors for %p." % [ self.unknown ]
  724. return self.unknown.collect do |field|
  725. "Unknown parameter '%s'" % [ field.capitalize ]
  726. end
  727. end
  728. ### Return a new ParamValidator with the additional +params+ merged into
  729. ### its values and re-validated.
  730. def merge( params )
  731. copy = self.dup
  732. copy.merge!( params )
  733. return copy
  734. end
  735. ### Merge the specified +params+ into the receiving ParamValidator and
  736. ### re-validate the resulting values.
  737. def merge!( params )
  738. return if params.empty?
  739. self.log.debug "Merging parameters for revalidation: %p" % [ params ]
  740. self.revalidate( params )
  741. end
  742. ### Returns an array containing valid parameters in the validator corresponding to the
  743. ### given +selector+(s).
  744. def values_at( *selector )
  745. selector.map!( &:to_sym )
  746. return self.valid.values_at( *selector )
  747. end
  748. #######
  749. private
  750. #######
  751. ### Build a deep hash out of the given parameter +value+
  752. def build_deep_hash( value, hash, levels )
  753. if levels.length == 0
  754. value.untaint
  755. elsif hash.nil?
  756. { levels.first => build_deep_hash(value, nil, levels[1..-1]) }
  757. else
  758. hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) })
  759. end
  760. end
  761. ### Get the number of hash levels in the specified +key+
  762. ### Stolen from the CGIMethods class in Rails' action_controller.
  763. def get_levels( key )
  764. all, main, bracketed, trailing = PARAMS_HASH_RE.match( key ).to_a
  765. if main.nil?
  766. return []
  767. elsif trailing
  768. return [key.untaint]
  769. elsif bracketed
  770. return [main.untaint] + bracketed.slice(1...-1).split('][').collect {|k| k.untaint }
  771. else
  772. return [main.untaint]
  773. end
  774. end
  775. end # class Strelka::ParamValidator