Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lib/goo/validators/enforce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ def enforce(inst,attr,value)
when :existence
check Goo::Validators::Existence, inst, attr, value, opt
when :list, Array
check Goo::Validators::DataType, inst, attr, value,opt, Array
check Goo::Validators::DataType, inst, attr, value, opt, Array
when :uri, RDF::URI
check Goo::Validators::DataType, inst, attr, value,opt, RDF::URI
check Goo::Validators::DataType, inst, attr, value, opt, RDF::URI
when :string, String
check Goo::Validators::DataType, inst, attr, value,opt, String
check Goo::Validators::DataType, inst, attr, value, opt, String
when :integer, Integer
check Goo::Validators::DataType, inst, attr, value,opt, Integer
check Goo::Validators::DataType, inst, attr, value, opt, Integer
when :boolean
check Goo::Validators::DataType, inst, attr, value, opt,:boolean
when :date_time, DateTime
Expand All @@ -43,6 +43,8 @@ def enforce(inst,attr,value)
check Goo::Validators::Symmetric, inst, attr, value, opt
when :email
check Goo::Validators::Email, inst, attr, value, opt
when :username
check Goo::Validators::Username, inst, attr, value, opt
when /^distinct_of_/
check Goo::Validators::DistinctOf, inst, attr, value, opt, opt
when /^superior_equal_to_/
Expand All @@ -54,6 +56,8 @@ def enforce(inst,attr,value)
when /^max_/, /^min_/
type = opt.to_s.index("max_") ? :max : :min
check Goo::Validators::ValueRange, inst, attr, value, type, opt.to_s
when /^safe_text/
check Goo::Validators::SafeText, inst, attr, value, opt, opt.to_s
else
if object_type?(opt)
check_object_type inst, attr, value, opt
Expand Down
41 changes: 34 additions & 7 deletions lib/goo/validators/implementations/email.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,48 @@ module Goo
module Validators
class Email < ValidatorBase
include Validator
EMAIL_REGEXP = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
# Matches reasonably valid emails (no double dots, no leading/trailing dots or hyphens, valid domain)
EMAIL_REGEXP = /\A
[a-z0-9!#$%&'*+\/=?^_`{|}~-]+ # local part
(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)* # dot-separated continuation in local
@
(?:(?!-)[a-z0-9-]{1,63}(?<!-)\.)+ # domain labels
[a-z]{2,} # top-level domain (at least 2 chars)
\z/ix

MIN_LENGTH = 6 # Smallest valid email: a@b.cd
MAX_LENGTH = 254 # RFC 5321 limits email length to 254 characters
LOCAL_PART_MAX = 64 # Per RFC
DOMAIN_PART_MAX = 253

key :email

error_message ->(obj) {
if @value.kind_of? Array
return "All values in attribute `#{@attr}` must be a valid emails"
return "All values in attribute `#{@attr}` must be valid email addresses"
else
return "Attribute `#{@attr}` with the value `#{@value}` must be a valid email"

return "Attribute `#{@attr}` with the value `#{@value}` must be a valid email address"
end
}

validity_check -> (obj) do
@value.nil? || @value.match?(EMAIL_REGEXP)
private

validity_check ->(obj) do
return true if @value.nil?

values = @value.is_a?(Array) ? @value : [@value]

values.all? do |email|
next false unless email.is_a?(String)
next false unless email.length.between?(MIN_LENGTH, MAX_LENGTH)

local, domain = email.split('@', 2)
next false if local.nil? || domain.nil?
next false if local.length > LOCAL_PART_MAX || domain.length > DOMAIN_PART_MAX

email.match?(EMAIL_REGEXP)
end
end
end
end
end
end
54 changes: 54 additions & 0 deletions lib/goo/validators/implementations/safe_text.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Goo
module Validators
class SafeText < ValidatorBase
include Validator

SAFE_TEXT_REGEX = /\A[\p{L}\p{N} .,'\-@()&!$%\/\[\]:;*+=?#^_{}|~"]+\z/u.freeze
DISALLOWED_UNICODE = /[\u0000-\u001F\u007F\u00A0\u200B-\u200F\u2028-\u202F\u202E\u2066-\u2069]/u.freeze

key :safe_text

error_message ->(obj) {
# Truncate long string values for clarity
truncated_value = if @value.is_a?(String) && @value.length > 60
"#{@value[0...57]}..."
else
@value
end

prefix = if @value.is_a?(Array)
"All values in attribute `#{@attr}`"
else
"Attribute `#{@attr}` with the value `#{truncated_value}`"
end

suffix = "must be safe text (no control or invisible Unicode characters, newlines, or disallowed punctuation)"
length_note = @max_length ? " and must not exceed #{@max_length} characters" : ""

"#{prefix} #{suffix}#{length_note}"
}

validity_check ->(obj) do
return true if @value.nil?

Array(@value).all? do |val|
next false unless val.is_a?(String)

length_ok = @max_length.nil? || val.length <= @max_length
length_ok &&
val !~ /\R/ &&
val =~ SAFE_TEXT_REGEX &&
val !~ DISALLOWED_UNICODE
end
end

def initialize(inst, attr, value, opt)
@max_length = nil
super(inst, attr, value)
match = opt.match(/_(\d+)$/)
@max_length = match[1].to_i if match && match[1]
end

end
end
end
45 changes: 45 additions & 0 deletions lib/goo/validators/implementations/username.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Goo
module Validators
class Username < ValidatorBase
include Validator

RESERVED_NAMES = %w[
admin administrator root support system test guest owner user
webmaster help contact host mail ftp info api noc security
].freeze

USERNAME_LENGTH_RANGE = (3..32).freeze

ASCII_ONLY_REGEX = /\A[\x20-\x7E]+\z/
USERNAME_PATTERN = /\A[a-zA-Z](?!.*[._]{2})[a-zA-Z0-9._]{1,30}[a-zA-Z0-9]\z/
INVISIBLE_CHARS = /[\u200B-\u200D\uFEFF]/

key :username

error_message ->(obj) {
base_msg = if @value.is_a?(Array)
"All values in attribute `#{@attr}` must be valid usernames"
else
"Attribute `#{@attr}` with the value `#{@value}` must be a valid username"
end
"#{base_msg} (must be 3–32 chars, start with a letter, contain only ASCII letters/digits/dots/underscores, no invisible or reserved terms)"
}

validity_check ->(obj) do
return true if @value.nil?

Array(@value).all? do |username|
next false unless username.is_a?(String)

username = username.strip

USERNAME_LENGTH_RANGE.cover?(username.length) &&
username.match?(ASCII_ONLY_REGEX) &&
username.match?(USERNAME_PATTERN) &&
!username.match?(INVISIBLE_CHARS) &&
!RESERVED_NAMES.include?(username.downcase)
end
end
end
end
end
85 changes: 85 additions & 0 deletions test/test_email_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require_relative 'test_case.rb'

module Goo
module Validators
class TestEmail < MiniTest::Unit::TestCase

def dummy_instance
@dummy ||= Object.new
end

def validate(value)
Email.new(dummy_instance, :email, value)
end

def assert_valid(value)
validator = validate(value)
assert validator.valid?, "Expected #{value.inspect} to be valid"
end

def assert_invalid(value)
validator = validate(value)
refute validator.valid?, "Expected #{value.inspect} to be invalid"
end

def test_valid_emails
assert_valid nil
assert_valid "user@example.com"
assert_valid "john.doe+test@sub.domain.org"
assert_valid "a_b-c@foo-bar.co.uk"
assert_valid "user123@domain.io"
end

def test_invalid_emails_structure
assert_invalid ""
assert_invalid "plainaddress"
assert_invalid "user@localhost"
assert_invalid "user@com"
assert_invalid "user@.com"
assert_invalid "user@com."
assert_invalid "user@-domain.com"
assert_invalid "user@domain-.com"
assert_invalid "user.@example.com"
assert_invalid "user..user@example.com"
assert_invalid "user@domain..com"
assert_invalid "user@"
end

def test_email_length_limits
too_short = "a@b.c" # 5 chars
assert_invalid too_short

long_local = "a" * 65
assert_invalid "#{long_local}@example.com"

long_domain = ("a" * 63 + ".") * 4 + "com"
assert_invalid "user@#{long_domain}"

too_long = "#{'a'*64}@#{'b'*189}.com" # 258 chars
assert_invalid too_long
end

def test_array_with_all_valid_emails
validator = validate(["valid@example.com", "foo.bar@domain.co"])
assert validator.valid?
end

def test_array_with_one_invalid_email
validator = validate(["good@domain.com", "bad@domain..com"])
refute validator.valid?
end

def test_error_message_for_single_invalid_email
validator = validate("invalid-email")
refute validator.valid?
assert_match(/must be a valid email address/i, validator.error)
end

def test_error_message_for_array_with_invalid
validator = validate(["invalid@", "also@bad"])
refute validator.valid?
assert_match(/All values.*must be valid email addresses/i, validator.error)
end
end
end
end
Loading