Class: Arrow::FormValidator

Inherits:
FormValidator
  • Object
show all
Extended by:
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

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 collapse

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(profile, params = nil) ⇒ FormValidator

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

#raw_formObject (readonly)

Returns the value of attribute raw_form.



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

def raw_form
  @raw_form
end

Instance Method Details

#[](key) ⇒ Object

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

#[]=(key, val) ⇒ Object

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

#apply_hash_constraint(key, constraint) ⇒ Object

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

#apply_proc_constraint(key, constraint, *params) ⇒ Object

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

#apply_regexp_constraint(key, constraint) ⇒ Object

Applies regexp constraint to form



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

#apply_string_constraint(key, constraint) ⇒ Object

Applies a builtin constraint to form.



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

#args?Boolean

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

#check_profile_syntax(profile) ⇒ Object

Overridden to remove the check for extra keys.



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

def check_profile_syntax( profile )
end

#descriptionsObject

Hash of field descriptions



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

def descriptions
	@profile[:descriptions]
end

#descriptions=(new_descs) ⇒ Object

Set hash of field descriptions



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

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

#do_constraint(key, constraints) ⇒ Object

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

#empty?Boolean

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

#error_fieldsObject

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

#error_messages(include_unknown = false) ⇒ Object

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

#errors?Boolean 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

#filtersObject

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

#get_description(field) ⇒ Object

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

#match_alpha(val) ⇒ Object

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

#match_alphanumeric(val) ⇒ Object

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

#match_boolean(val) ⇒ Object

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

#match_date(val) ⇒ Object

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

#match_email(val) ⇒ Object

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

#match_float(val) ⇒ Object

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

#match_hostname(val) ⇒ Object

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

#match_integer(val) ⇒ Object

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

#match_printable(val) ⇒ Object

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

#match_uri(val) ⇒ Object

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

#missingObject

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

#okay?Boolean

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

#set_form_value(key, value, constraint) ⇒ Object

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

#to_sObject

Stringified description of the validator



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

def to_s
	""
end

#unknownObject

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

#untaint?(field) ⇒ Boolean

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

#validObject

Returns the valid fields after expanding Rails-style ‘customer[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

#validate(params, additional_profile = nil) ⇒ Object

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