Class: Arrow::FormValidator

Inherits:
FormValidator
  • Object
show all
Extends:
Forwardable
Includes:
Loggable
Defined in:
lib/arrow/formvalidator.rb

Overview

A FormValidator variant that adds some convenience methods and additional validations.

Usage

  require 'arrow/formvalidator'

# Profile specifies validation criteria for input profile = {

    :required   => :name,
    :optional   => [:email, :description],
    :filters    => [:strip, :squeeze],
    :untaint_all_constraints => true,
    :descriptions => {
      :email     => "Customer Email",
      :description => "Issue Description",
      :name      => "Customer Name",
    },
    :constraints  => {
      :email => :email,
      :name  => /^[\x20-\x7f]+$/,
      :description => /^[\x20-\x7f]+$/,
    },

}

# Create a validator object and pass in a hash of request parameters and the # profile hash.

  validator = Arrow::FormValidator.new

validator.validate( req_params, profile )

# Now if there weren’t any errors, send the success page if validator.okay?

  return success_template

# Otherwise fill in the error template with auto-generated error messages # and return that instead. else

  failure_template.errors( validator.error_messages )
  return failure_template

end

VCS Id

$Id: formvalidator.rb,v 1b8226c06192 2010/08/09 17:50:38 ged $

Authors

  • Michael Granger

Please see the file LICENSE in the top-level directory for licensing details.

Portions of this file are from Ruby on Rails’ CGIMethods class from the action_controller:

  
  Copyright (c) 2004 David Heinemeier Hansson
  
  Permission is hereby granted, free of charge, to any person obtaining
  a copy of this software and associated documentation files (the
  "Software"), to deal in the Software without restriction, including
  without limitation the rights to use, copy, modify, merge, publish,
  distribute, sublicense, and/or sell copies of the Software, and to
  permit persons to whom the Software is furnished to do so, subject to
  the following conditions:
  
  The above copyright notice and this permission notice shall be
  included in all copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  

Please see the file COPYRIGHT in the ‘docs’ directory for licensing details.

Constant Summary

Defaults =
{
  :descriptions => {},
}
RFC822_EMAIL_ADDRESS =

RFC822 Email Address Regex


Originally written by Cal Henderson c.f. iamcal.com/publish/articles/php/parsing_email/

Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.

Licensed under a Creative Commons Attribution-ShareAlike 2.5 License creativecommons.org/licenses/by-sa/2.5/

begin
  qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
  dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
  atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
    '\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
  quoted_pair = '\\x5c[\\x00-\\x7f]'
  domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
  quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
  domain_ref = atom
  sub_domain = "(?:#{domain_ref}|#{domain_literal})"
  word = "(?:#{atom}|#{quoted_string})"
  domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
  local_part = "#{word}(?:\\x2e#{word})*"
  addr_spec = "#{local_part}\\x40#{domain}"
  /\A#{addr_spec}\z/
end
RFC1738Hostname =
begin
  alphadigit = /[a-z0-9]/i
  # toplabel     = alpha | alpha *[ alphadigit | "-" ] alphadigit
  toplabel = /[a-z]((#{alphadigit}|-)*#{alphadigit})?/i
  # domainlabel  = alphadigit | alphadigit *[ alphadigit | "-" ] alphadigit
  domainlabel = /#{alphadigit}((#{alphadigit}|-)*#{alphadigit})?/i
  # hostname     = *[ domainlabel "." ] toplabel
  hostname = /\A(#{domainlabel}\.)*#{toplabel}\z/
end
PARAMS_HASH_RE =

Get the number of hash levels in the specified key Stolen from the CGIMethods class in Rails’ action_controller.

/^([^\[]+)(\[.*\])?(.)?.*$/

Instance Attribute Summary

Instance Method Summary

Methods included from Loggable

#log

Constructor Details

- (FormValidator) initialize(profile, params = nil)

Create a new Arrow::FormValidator object.



104
105
106
107
# File 'lib/arrow/formvalidator.rb', line 104

def initialize( profile, params=nil )
  @profile = Defaults.merge( profile )
  validate( params ) if params
end

Instance Attribute Details

- (Object) raw_form (readonly)



114
115
116
# File 'lib/arrow/formvalidator.rb', line 114

def raw_form
  @raw_form
end

Instance Method Details

- (Object) [](key)

Index operator; fetch the validated value for form field key.



160
161
162
# File 'lib/arrow/formvalidator.rb', line 160

def []( key )
  @form[ key.to_s ]
end

- (Object) []=(key, val)

Index assignment operator; set the validated value for form field key to the specified val.



167
168
169
# File 'lib/arrow/formvalidator.rb', line 167

def []=( key, val )
  @form[ key.to_s ] = val
end

- (Object) apply_hash_constraint(key, constraint)

Apply a constraint given as a Hash to the value/s corresponding to the specified key:

constraint

A builtin constraint (as a Symbol; e.g., :email), a Regexp, or a Proc.

name

A description of the constraint should it fail and be listed in #invalid.

params

If constraint is a Proc, this field should contain a list of other fields to send to the Proc.



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/arrow/formvalidator.rb', line 462

def apply_hash_constraint( key, constraint )
  action = constraint["constraint"]

  rval = case action
    when String
      self.apply_string_constraint( key, action )
    when Regexp
      self.apply_regexp_constraint( key, action )
    when Proc
      if args = constraint["params"]
        args.collect! {|field| @form[field] }
        self.apply_proc_constraint( key, action, *args )
      else
        self.apply_proc_constraint( key, action )
      end
    end

  # If the validation failed, and there's a name for this constraint, replace
  # the name in @invalid_fields with the name
  if !rval && constraint["name"]
    @invalid_fields[key] = constraint["name"]
  end

  return rval
end

- (Object) apply_proc_constraint(key, constraint, *params)

Apply a constraint that was specified as a Proc to the value for the given key



491
492
493
494
495
496
497
498
499
500
501
# File 'lib/arrow/formvalidator.rb', line 491

def apply_proc_constraint( key, constraint, *params )
  value = nil

  unless params.empty?
    value = constraint.call( *params )
  else
    value = constraint.call( @form[key] )
  end

  self.set_form_value( key, value, constraint )
end

- (Object) apply_regexp_constraint(key, constraint)

Applies regexp constraint to form[key]



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/arrow/formvalidator.rb', line 505

def apply_regexp_constraint( key, constraint )
  self.log.debug "Validating '%p' via regexp %p" % [@form[key], constraint]

  if match = constraint.match( @form[key].to_s )
    self.log.debug "  matched %p" % [match[0]]

    if match.captures.empty?
      self.log.debug "  no captures, using whole match: %p" % [match[0]]
      self.set_form_value( key, match[0], constraint )
    elsif match.captures.length == 1
      self.log.debug "  extracting one capture: %p" % [match.captures.first]
      self.set_form_value( key, match.captures.first, constraint )
    else
      self.log.debug "  extracting multiple captures: %p" % [match.captures]
      self.set_form_value( key, match.captures, constraint )
    end
  else
    self.set_form_value( key, nil, constraint )
  end
end

- (Object) apply_string_constraint(key, constraint)

Applies a builtin constraint to form[key].



443
444
445
446
447
448
449
# File 'lib/arrow/formvalidator.rb', line 443

def apply_string_constraint( key, constraint )
  # FIXME: multiple elements
  rval = self.__send__( "match_#{constraint}", @form[key].to_s )
  self.log.debug "Tried a string constraint: %p: %p" %
    [ @form[key].to_s, rval ]
  self.set_form_value( key, rval, constraint )
end

- (Boolean) args?

Returns true if there were arguments given.

Returns:

  • (Boolean)


179
180
181
# File 'lib/arrow/formvalidator.rb', line 179

def args?
  return !@form.empty?
end

- (Object) check_profile_syntax(profile)

Overridden to remove the check for extra keys.



155
156
# File 'lib/arrow/formvalidator.rb', line 155

def check_profile_syntax( profile )
end

- (Object) descriptions

Hash of field descriptions



128
129
130
# File 'lib/arrow/formvalidator.rb', line 128

def descriptions
  @profile[:descriptions]
end

- (Object) descriptions=(new_descs)

Set hash of field descriptions



134
135
136
# File 'lib/arrow/formvalidator.rb', line 134

def descriptions=( new_descs )
  @profile[:descriptions] = new_descs
end

- (Object) do_constraint(key, constraints)

Apply one or more constraints to the field value/s corresponding to key.



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/arrow/formvalidator.rb', line 424

def do_constraint( key, constraints )
  constraints.each do |constraint|
    case constraint
    when String
      apply_string_constraint( key, constraint )
    when Hash
      apply_hash_constraint( key, constraint )
    when Proc
      apply_proc_constraint( key, constraint )
    when Regexp
      apply_regexp_constraint( key, constraint ) 
    else
      raise "unknown constraint type %p" % [constraint]
    end
  end
end

- (Boolean) empty?

Returns true if there were no arguments given.

Returns:

  • (Boolean)


173
174
175
# File 'lib/arrow/formvalidator.rb', line 173

def empty?
  return @form.empty?
end

- (Object) error_fields

Return an array of field names which had some kind of error associated with them.



215
216
217
# File 'lib/arrow/formvalidator.rb', line 215

def error_fields
  return self.missing | self.invalid.keys
end

- (Object) error_messages(include_unknown = false)

Return an error message for each missing or invalid field; if includeUnknown is true, also include messages for unknown fields.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/arrow/formvalidator.rb', line 235

def error_messages( include_unknown=false )
  self.log.debug "Building error messages from descriptions: %p" %
    [ @profile[:descriptions] ]
  msgs = []
  self.missing.each do |field|
    msgs << "Missing value for '%s'" % self.get_description( field )
  end

  self.invalid.each do |field, constraint|
    msgs << "Invalid value for '%s'" % self.get_description( field )
  end

  if include_unknown
    self.unknown.each do |field|
      msgs << "Unknown parameter '%s'" % self.get_description( field )
    end
  end

  return msgs
end

- (Boolean) errors? Also known as: has_errors?

Returns true if any fields are missing or contain invalid values.

Returns:

  • (Boolean)


185
186
187
# File 'lib/arrow/formvalidator.rb', line 185

def errors?
  return !self.okay?
end

- (Object) filters

Formvalidator hack: The formvalidator filters method has a bug where he assumes an array

  when it is in fact a string for multiple values (ie anytime you have a 
  text-area with newlines in it).


559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/arrow/formvalidator.rb', line 559

def filters
  @filters_array = Array(@profile[:filters]) unless(@filters_array)
  @filters_array.each do |filter|

    if respond_to?( "filter_#{filter}" )
      @form.keys.each do |field|
        # If a key has multiple elements, apply filter to each element
        @field_array = Array( @form[field] )

        if @field_array.length > 1
          @field_array.each_index do |i|
            elem = @field_array[i]
            @field_array[i] = self.send("filter_#{filter}", elem)
          end
        else
          if not @form[field].to_s.empty?
            @form[field] = self.send("filter_#{filter}", @form[field].to_s)
          end
        end
      end
    end
  end
  @form
end

- (Object) get_description(field)

Get the description for the specified field.



221
222
223
224
225
226
227
228
229
230
# File 'lib/arrow/formvalidator.rb', line 221

def get_description( field )
  return @profile[:descriptions][ field.to_s ] if
    @profile[:descriptions].key?( field.to_s )

  desc = field.to_s.
    gsub( /.*\[(\w+)\]/, "\\1" ).
    gsub( /_(.)/ ) {|m| " " + m[1,1].upcase }.
    gsub( /^(.)/ ) {|m| m.upcase }
  return desc
end

- (Object) match_alpha(val)

Constrain a value to alpha characters (a-z, case-insensitive)



324
325
326
327
328
329
330
# File 'lib/arrow/formvalidator.rb', line 324

def match_alpha( val )
  if val =~ /^([a-z]+)$/i
    return $1
  else
    return nil
  end
end

- (Object) match_alphanumeric(val)

Constrain a value to alpha characters (a-z, case-insensitive and 0-9)



334
335
336
337
338
339
340
# File 'lib/arrow/formvalidator.rb', line 334

def match_alphanumeric( val )
  if val =~ /^([a-z0-9]+)$/i
    return $1
  else
    return nil
  end
end

- (Object) match_boolean(val)

Constrain a value to true (or yes) and false (or no).



293
294
295
296
297
298
299
300
301
302
# File 'lib/arrow/formvalidator.rb', line 293

def match_boolean( val )
  rval = nil
  if ( val =~ /^(t(?:rue)?|y(?:es)?)|1$/i )
    rval = true
  elsif ( val =~ /^(no?|f(?:alse)?)|0$/i )
    rval = false
  end

  return rval
end

- (Object) match_date(val)

Constrain a value to a parseable Date



318
319
320
# File 'lib/arrow/formvalidator.rb', line 318

def match_date( val )
  return Date.parse( val ) rescue nil
end

- (Object) match_email(val)

Override the parent class’s definition to (not-sloppily) match email addresses.



385
386
387
388
389
390
# File 'lib/arrow/formvalidator.rb', line 385

def match_email( val )
  match = RFC822_EMAIL_ADDRESS.match( val )
  self.log.debug "Validating an email address %p: %p" %
    [ val, match ]
  return match ? match[0] : nil
end

- (Object) match_float(val)

Contrain a value to a Float



312
313
314
# File 'lib/arrow/formvalidator.rb', line 312

def match_float( val )
  return Float( val ) rescue nil
end

- (Object) match_hostname(val)

Match valid hostnames according to the rules of the URL RFC.



404
405
406
407
# File 'lib/arrow/formvalidator.rb', line 404

def match_hostname( val )
  match = RFC1738Hostname.match( val )
  return match ? match[0] : nil
end

- (Object) match_integer(val)

Constrain a value to an integer



306
307
308
# File 'lib/arrow/formvalidator.rb', line 306

def match_integer( val )
  return Integer( val ) rescue nil
end

- (Object) match_printable(val)

Constrain a value to any printable characters



344
345
346
347
348
349
350
# File 'lib/arrow/formvalidator.rb', line 344

def match_printable( val )
  if val =~ /^([[:print:][:space:]]{0,255})$/
    return val
  else
    return nil
  end
end

- (Object) match_uri(val)

Match valid URIs



411
412
413
414
415
416
417
418
419
# File 'lib/arrow/formvalidator.rb', line 411

def match_uri( val )
  return URI.parse( val )
rescue URI::InvalidURIError => err
  self.log.error "Error trying to parse URI %p: %s" % [ val, err.message ]
  return nil
rescue NoMethodError
  self.log.debug "Ignoring bug in URI#parse"
  return nil
end

- (Object) missing

Returns a distinct list of missing fields. Overridden to eliminate the “undefined method `<=>’ for :foo:Symbol” error.



259
260
261
# File 'lib/arrow/formvalidator.rb', line 259

def missing
  @missing_fields.uniq.sort_by {|f| f.to_s}
end

- (Boolean) okay?

Return true if all required fields were present and validated correctly.

Returns:

  • (Boolean)


193
194
195
# File 'lib/arrow/formvalidator.rb', line 193

def okay?
  self.missing.empty? && self.invalid.empty?
end

- (Object) set_form_value(key, value, constraint)

Set the form value for the given key. If value is false, add it to the list of invalid fields with a description derived from the specified constraint.



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'lib/arrow/formvalidator.rb', line 530

def set_form_value( key, value, constraint )
  key.untaint

  if !value.nil? 
    self.log.debug "Setting form value for %p to %p (constraint was %p)" %
      [ key, value, constraint ]
    @form[key] = value
    @form[key].untaint if self.untaint?( key )
    return true

  else
    self.log.debug "Clearing form value for %p (constraint was %p)" %
      [ key, constraint ]
    @form.delete( key )
    @invalid_fields ||= {}
    @invalid_fields[ key ] ||= []

    unless @invalid_fields[ key ].include?( constraint )
      @invalid_fields[ key ].push( constraint )
    end
    return false
  end
end

- (Object) to_s

Stringified description of the validator



123
124
125
# File 'lib/arrow/formvalidator.rb', line 123

def to_s
  ""
end

- (Object) unknown

Returns a distinct list of unknown fields.



264
265
266
# File 'lib/arrow/formvalidator.rb', line 264

def unknown
  (@unknown_fields - @invalid_fields.keys).uniq.sort_by {|f| f.to_s}
end

- (Boolean) untaint?(field)

Returns true if the given field is one that should be untainted.

Returns:

  • (Boolean)


199
200
201
202
203
204
205
206
207
208
209
# File 'lib/arrow/formvalidator.rb', line 199

def untaint?( field )
  self.log.debug "Checking to see if %p should be untainted." % [field]
  rval = ( @untaint_all || @untaint_fields.include?(field) )
  if rval
    self.log.debug "  ...yep it should."
  else
    self.log.debug "  ...nope."
  end

  return rval
end

- (Object) valid

Returns the valid fields after expanding Rails-style ‘customer[address][street]’ variables into multi-level hashes.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/arrow/formvalidator.rb', line 271

def valid
  if @parsed_params.nil?
    @parsed_params = {}
    valid = super()

    for key, value in valid
      value = [value] if key =~ /.*\[\]$/
      unless key.include?( '[' )
        @parsed_params[ key ] = value
      else
        build_deep_hash( value, @parsed_params, get_levels(key) )
      end
    end
  end

  return @parsed_params
end

- (Object) validate(params, additional_profile = nil)

Validate the input in params. If the optional additional_profile is given, merge it with the validator’s default profile before validating.



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/arrow/formvalidator.rb', line 141

def validate( params, additional_profile=nil )
  @raw_form = params.dup
  profile = @profile

  if additional_profile
    self.log.debug "Merging additional profile %p" % [additional_profile]
    profile = @profile.merge( additional_profile ) 
  end

  super( params, profile )
end