validators 3.0.5 → 3.3.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +8 -0
- data/README.md +47 -3
- data/bin/sync-disposable-hostnames +218 -23
- data/bin/sync-tld +11 -8
- data/data/country_tlds.txt +235 -0
- data/data/disposable_domains.txt +110996 -0
- data/data/disposable_emails.txt +38 -0
- data/data/reserved_subdomains.txt +2830 -0
- data/data/tld.txt +1508 -0
- data/lib/validators.rb +7 -0
- data/lib/validators/constants.rb +1 -1
- data/lib/validators/disposable_emails.rb +19 -0
- data/lib/validators/disposable_hostnames.rb +2 -2
- data/lib/validators/locale/en.yml +30 -0
- data/lib/validators/locale/pt-BR.yml +28 -0
- data/lib/validators/reserved_subdomains.rb +47 -0
- data/lib/validators/tld.rb +2 -2
- data/lib/validators/validates_email_format_of.rb +19 -4
- data/lib/validators/validates_subdomain.rb +69 -0
- data/lib/validators/validates_username.rb +15 -0
- data/lib/validators/version.rb +2 -2
- data/test/support/models.rb +12 -0
- data/test/test_helper.rb +20 -14
- data/test/validators/disposable_email_test.rb +19 -5
- data/test/validators/validates_email_format_of_test.rb +4 -2
- data/test/validators/validates_subdomain_test.rb +75 -0
- data/test/validators/validates_username_test.rb +63 -0
- data/validators.gemspec +9 -1
- metadata +104 -9
- data/data/disposable.json +0 -57281
- data/data/tld.json +0 -1516
- data/test/support/translations.yml +0 -39
data/lib/validators.rb
CHANGED
@@ -8,6 +8,9 @@ module Validators
|
|
8
8
|
require "validators/tld"
|
9
9
|
require "validators/hostname"
|
10
10
|
require "validators/disposable_hostnames"
|
11
|
+
require "validators/disposable_emails"
|
12
|
+
require "validators/reserved_subdomains"
|
13
|
+
|
11
14
|
require "validators/validates_datetime"
|
12
15
|
require "validators/validates_ip_address"
|
13
16
|
require "validators/validates_email_format_of"
|
@@ -18,4 +21,8 @@ module Validators
|
|
18
21
|
require "validators/validates_ssh_private_key"
|
19
22
|
require "validators/validates_ssh_public_key"
|
20
23
|
require "validators/validates_hostname_format_of"
|
24
|
+
require "validators/validates_subdomain"
|
25
|
+
require "validators/validates_username"
|
26
|
+
|
27
|
+
I18n.load_path += Dir[File.join(__dir__, "validators/locale/*.yml")]
|
21
28
|
end
|
data/lib/validators/constants.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Validators
|
4
4
|
EMAIL_FORMAT = /\A[a-z0-9]+([-._][a-z0-9]+)*(\+[^@]+)?@[a-z0-9]+([.-][a-z0-9]+)*\.[a-z]{2,}\z/i.freeze
|
5
|
-
MICROSOFT_EMAIL_FORMAT = /\A[
|
5
|
+
MICROSOFT_EMAIL_FORMAT = /\A[a-z0-9][a-z0-9._-]*[a-z0-9_-]+(\+[a-z0-9]+)?@(hotmail|outlook).com\z/i.freeze
|
6
6
|
|
7
7
|
# Source: https://github.com/henrik/validates_url_format_of
|
8
8
|
IPV4_PART = /\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]/.freeze # 0-255
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Validators
|
4
|
+
class DisposableEmails
|
5
|
+
FILE_PATH = File.expand_path("../../data/disposable_emails.txt", __dir__)
|
6
|
+
|
7
|
+
def self.all
|
8
|
+
@all ||= File.read(FILE_PATH).lines.map(&:chomp)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.include?(email)
|
12
|
+
mailbox, domain = email.to_s.split("@")
|
13
|
+
mailbox = mailbox.to_s.gsub(".", "")
|
14
|
+
mailbox = mailbox.gsub(/\+(.+)?\Z/, "")
|
15
|
+
|
16
|
+
all.include?("#{mailbox}@#{domain}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Validators
|
4
4
|
class DisposableHostnames
|
5
|
-
FILE_PATH = File.expand_path("../../data/
|
5
|
+
FILE_PATH = File.expand_path("../../data/disposable_domains.txt", __dir__)
|
6
6
|
|
7
7
|
def self.all
|
8
|
-
@all ||=
|
8
|
+
@all ||= File.read(FILE_PATH).lines.map(&:chomp)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
---
|
2
|
+
en:
|
3
|
+
activemodel: &activemodel
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
disposable_email: "is not allowed (high-bounce email)"
|
7
|
+
disposable_domain: "is not allowed (high-bounce domain)"
|
8
|
+
invalid_cnpj: "is not a valid CNPJ"
|
9
|
+
invalid_cpf: "is not a valid CPF"
|
10
|
+
invalid_date: "is not a valid date"
|
11
|
+
invalid_date_after: "needs to be after %{date}"
|
12
|
+
invalid_date_before: "needs to be before %{date}"
|
13
|
+
invalid_email: "is not a valid address"
|
14
|
+
invalid_hostname: "does not have a valid hostname"
|
15
|
+
invalid_ip_address: "is not a valid IP address"
|
16
|
+
invalid_ipv4_address: "is not a valid IPv4 address"
|
17
|
+
invalid_ipv6_address: "is not a valid IPv6 address"
|
18
|
+
invalid_owner: "is not associated with your user"
|
19
|
+
invalid_ssh_private_key: "is not a valid private SSH key"
|
20
|
+
invalid_ssh_private_key_bits: "needs to be at least %{required} bits; got %{value} bits instead"
|
21
|
+
invalid_ssh_private_key_type: "must be a %{value} key"
|
22
|
+
invalid_ssh_public_key: "is not a valid public SSH key"
|
23
|
+
invalid_url: "is not a valid address"
|
24
|
+
reserved_subdomain: "%{value} is a reserved subdomain"
|
25
|
+
invalid_subdomain: "is invalid"
|
26
|
+
reserved_username: "%{value} is a reserved username"
|
27
|
+
invalid_username: "is invalid"
|
28
|
+
|
29
|
+
activerecord:
|
30
|
+
<<: *activemodel
|
@@ -0,0 +1,28 @@
|
|
1
|
+
---
|
2
|
+
pt-BR:
|
3
|
+
activemodel: &activemodel
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
disposable_email: "não é permitido (e-mail temporário)"
|
7
|
+
disposable_domain: "não é permitido (e-mail temporário)"
|
8
|
+
invalid_cnpj: "não é um CNPJ válido"
|
9
|
+
invalid_cpf: "não é um CPF válido"
|
10
|
+
invalid_date: "não é uma data válida"
|
11
|
+
invalid_date_after: "precisa ser depois de %{date}"
|
12
|
+
invalid_date_before: "precisa ser antes de %{date}"
|
13
|
+
invalid_email: "não parece ser um e-mail válido"
|
14
|
+
invalid_hostname: "não é um hostname válido"
|
15
|
+
invalid_ip_address: "não é um endereço IP válido"
|
16
|
+
invalid_ipv4_address: "não é um endereço IPv4 válido"
|
17
|
+
invalid_ipv6_address: "não é um endereço IPv6 válido"
|
18
|
+
invalid_owner: "não está associado ao seu usuário"
|
19
|
+
invalid_ssh_private_key: "não é uma chave privada de SSH válida"
|
20
|
+
invalid_ssh_private_key_bits: "precisa ter pelo menos %{required} bits; a sua chave tem %{value} bits"
|
21
|
+
invalid_ssh_private_key_type: "precisa ser uma chave %{value}"
|
22
|
+
invalid_ssh_public_key: "não é uma chave pública de SSH válida"
|
23
|
+
invalid_url: "não parece ser uma URL válida"
|
24
|
+
reserved_hostname: "%{value} é um hostname reservado"
|
25
|
+
reserved_username: "%{value} é nome de usuário reservado"
|
26
|
+
|
27
|
+
activerecord:
|
28
|
+
<<: *activemodel
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Validators
|
4
|
+
class ReservedSubdomains
|
5
|
+
FILE_PATH = File.expand_path("../../data/reserved_subdomains.txt", __dir__)
|
6
|
+
|
7
|
+
def self.reserved?(hostname, matchers = nil)
|
8
|
+
matchers = parse_list(matchers) if matchers
|
9
|
+
matchers ||= all
|
10
|
+
match_any?(matchers, hostname)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.all
|
14
|
+
@all ||= File.read(FILE_PATH).lines.map {|matcher| parse(matcher.chomp) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse(matcher)
|
18
|
+
return matcher unless matcher.start_with?("/")
|
19
|
+
|
20
|
+
Regexp.compile(matcher[%r{/(.*?)/}, 1])
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_list(matchers)
|
24
|
+
matchers.map {|matcher| parse(matcher) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.match_any?(matchers, hostname)
|
28
|
+
hostname = normalize(hostname)
|
29
|
+
matchers.any? {|matcher| match?(matcher, hostname) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.normalize(hostname)
|
33
|
+
hostname.downcase.gsub(/[_-]/, "")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.match?(matcher, hostname)
|
37
|
+
case matcher
|
38
|
+
when String
|
39
|
+
matcher == hostname
|
40
|
+
when Regexp
|
41
|
+
hostname =~ matcher
|
42
|
+
else
|
43
|
+
raise "Unknown matcher type: #{matcher.class}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/validators/tld.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Validators
|
4
4
|
class TLD
|
5
|
-
FILE_PATH = File.expand_path("../../data/tld.
|
5
|
+
FILE_PATH = File.expand_path("../../data/tld.txt", __dir__)
|
6
6
|
|
7
7
|
def self.all
|
8
|
-
@all ||=
|
8
|
+
@all ||= File.read(FILE_PATH).lines.map(&:chomp)
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.host_with_valid_tld?(host)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "root_domain"
|
4
|
+
|
3
5
|
module ActiveModel
|
4
6
|
module Validations
|
5
7
|
class EmailValidator < EachValidator
|
@@ -14,6 +16,7 @@ module ActiveModel
|
|
14
16
|
|
15
17
|
validate_tld(record, attribute, value, options) if check_tld
|
16
18
|
validate_email_format(record, attribute, value, options)
|
19
|
+
validate_disposable_domain(record, attribute, value, options) unless allow_disposable
|
17
20
|
validate_disposable_email(record, attribute, value, options) unless allow_disposable
|
18
21
|
end
|
19
22
|
|
@@ -41,12 +44,24 @@ module ActiveModel
|
|
41
44
|
)
|
42
45
|
end
|
43
46
|
|
44
|
-
def
|
47
|
+
def validate_disposable_domain(record, attribute, value, _options)
|
48
|
+
return unless value
|
49
|
+
|
45
50
|
hostname = value.to_s.split(AT_SIGN).last.to_s.downcase
|
51
|
+
root_domain = RootDomain.call(hostname)
|
46
52
|
|
47
|
-
return
|
48
|
-
|
49
|
-
|
53
|
+
return unless Validators::DisposableHostnames.all.include?(root_domain)
|
54
|
+
|
55
|
+
record.errors.add(
|
56
|
+
attribute,
|
57
|
+
:disposable_domain,
|
58
|
+
value: value
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_disposable_email(record, attribute, value, _options)
|
63
|
+
return unless value
|
64
|
+
return unless Validators::DisposableEmails.include?(value)
|
50
65
|
|
51
66
|
record.errors.add(
|
52
67
|
attribute,
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
class SubdomainValidator < EachValidator
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
return if value.blank? && options[:allow_blank]
|
8
|
+
return if value.nil? && options[:allow_nil]
|
9
|
+
|
10
|
+
value = value.to_s
|
11
|
+
|
12
|
+
validate_reserved_subdomain(record, attribute, value)
|
13
|
+
validate_format(record, attribute, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def reserved?(subdomain)
|
17
|
+
::Validators::ReservedSubdomains.reserved?(subdomain, options[:in])
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_reserved_subdomain(record, attribute, value)
|
21
|
+
return unless options.fetch(:reserved, true)
|
22
|
+
return unless reserved?(value)
|
23
|
+
|
24
|
+
record.errors.add(
|
25
|
+
attribute,
|
26
|
+
:"reserved_#{options[:error_name]}",
|
27
|
+
message: options[:message],
|
28
|
+
value: value
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_format(record, attribute, value)
|
33
|
+
return if Validators::Hostname.valid_label?(value)
|
34
|
+
|
35
|
+
record.errors.add(
|
36
|
+
attribute,
|
37
|
+
:"invalid_#{options[:error_name]}",
|
38
|
+
message: options[:message],
|
39
|
+
value: value
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
# Validates whether or not the specified host label is valid.
|
46
|
+
# The `in: array` can have strings and patterns. A pattern is everything
|
47
|
+
# that starts with `/` and will be parsed as a regular expression.
|
48
|
+
#
|
49
|
+
# Notice that subdomains will be normalized; it'll be downcased and have
|
50
|
+
# its underscores and hyphens stripped before validating.
|
51
|
+
#
|
52
|
+
# class User < ActiveRecord::Base
|
53
|
+
# # Validates format and rejects reserved subdomains.
|
54
|
+
# validates_subdomain :subdomain
|
55
|
+
#
|
56
|
+
# # Validates against a custom list.
|
57
|
+
# validates_subdomain :subdomain, in: %w[www]
|
58
|
+
#
|
59
|
+
# # Rejects reserved domains validation.
|
60
|
+
# validates_subdomain :subdomain, reserved: false
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
def validates_subdomain(*attr_names)
|
64
|
+
options = _merge_attributes(attr_names).merge(error_name: :subdomain)
|
65
|
+
validates_with SubdomainValidator, options
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
module Validations
|
5
|
+
class UsernameValidator < SubdomainValidator
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def validates_username(*attr_names)
|
10
|
+
options = _merge_attributes(attr_names).merge(error_name: :username)
|
11
|
+
validates_with UsernameValidator, options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/validators/version.rb
CHANGED
data/test/support/models.rb
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
def build_model(&block)
|
4
|
+
Class.new do
|
5
|
+
include ActiveModel::Model
|
6
|
+
|
7
|
+
def self.name
|
8
|
+
"SomeModel"
|
9
|
+
end
|
10
|
+
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
3
15
|
class User < ActiveRecord::Base
|
4
16
|
has_many :tasks
|
5
17
|
has_many :categories
|
data/test/test_helper.rb
CHANGED
@@ -1,16 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "simplecov-console"
|
5
|
-
|
6
|
-
SimpleCov.minimum_coverage 100
|
7
|
-
SimpleCov.minimum_coverage_by_file 100
|
8
|
-
SimpleCov.refuse_coverage_drop
|
3
|
+
$VERBOSE = nil
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
SimpleCov::Formatter::HTMLFormatter
|
13
|
-
])
|
5
|
+
require "simplecov"
|
6
|
+
SimpleCov.start
|
14
7
|
|
15
8
|
SimpleCov.start do
|
16
9
|
add_filter "test/support"
|
@@ -24,18 +17,31 @@ require "active_support/all"
|
|
24
17
|
require "minitest/utils"
|
25
18
|
require "minitest/autorun"
|
26
19
|
|
20
|
+
def build_email_with_filter(email)
|
21
|
+
mailbox, domain = email.split("@")
|
22
|
+
"#{mailbox}+#{SecureRandom.hex(3)}@#{domain}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_email_with_dots(email)
|
26
|
+
mailbox, domain = email.split("@")
|
27
|
+
new_mailbox = mailbox.chars.map {|char| [char, "."] }.flatten[0..-2].join
|
28
|
+
|
29
|
+
"#{new_mailbox}@#{domain}"
|
30
|
+
end
|
31
|
+
|
27
32
|
Time.zone = "America/Sao_Paulo"
|
28
33
|
TLDs = Validators::TLD.all.sample(10)
|
29
|
-
|
34
|
+
DISPOSABLE_DOMAINS = Validators::DisposableHostnames.all.sample(10)
|
35
|
+
DISPOSABLE_EMAILS = Validators::DisposableEmails.all +
|
36
|
+
Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(email) } +
|
37
|
+
Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_dots(email) } +
|
38
|
+
Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(build_email_with_dots(email)) }
|
30
39
|
|
31
40
|
Dir[File.join(__dir__, "support/**/*.rb")].sort.each {|f| require f }
|
32
41
|
|
33
42
|
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
34
43
|
load "schema.rb"
|
35
44
|
|
36
|
-
I18n.enforce_available_locales = false
|
37
|
-
I18n.load_path << File.join(__dir__, "support/translations.yml")
|
38
|
-
|
39
45
|
module Minitest
|
40
46
|
class Test
|
41
47
|
setup do
|
@@ -3,25 +3,27 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class DisposableEmailTest < Minitest::Test
|
6
|
-
|
7
|
-
test "rejects disposable
|
6
|
+
DISPOSABLE_DOMAINS.each do |domain|
|
7
|
+
test "rejects disposable domain (#{domain})" do
|
8
8
|
User.validates_email_format_of :email
|
9
9
|
|
10
10
|
user = User.new(email: "user@#{domain}")
|
11
11
|
user.valid?
|
12
12
|
|
13
|
-
assert_includes user.errors[:email],
|
13
|
+
assert_includes user.errors[:email],
|
14
|
+
I18n.t("activerecord.errors.messages.disposable_domain")
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
+
DISPOSABLE_DOMAINS.each do |domain|
|
18
19
|
test "rejects disposable e-mail with subdomain (custom.#{domain})" do
|
19
20
|
User.validates_email_format_of :email
|
20
21
|
|
21
22
|
user = User.new(email: "user@custom.#{domain}")
|
22
23
|
user.valid?
|
23
24
|
|
24
|
-
assert_includes user.errors[:email],
|
25
|
+
assert_includes user.errors[:email],
|
26
|
+
I18n.t("activerecord.errors.messages.disposable_domain")
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
@@ -33,4 +35,16 @@ class DisposableEmailTest < Minitest::Test
|
|
33
35
|
|
34
36
|
assert user.errors[:email].empty?
|
35
37
|
end
|
38
|
+
|
39
|
+
DISPOSABLE_EMAILS.each do |email|
|
40
|
+
test "rejects disposable e-mail (#{email})" do
|
41
|
+
User.validates_email_format_of :email
|
42
|
+
|
43
|
+
user = User.new(email: email)
|
44
|
+
user.valid?
|
45
|
+
|
46
|
+
assert_includes user.errors[:email],
|
47
|
+
I18n.t("activerecord.errors.messages.disposable_email")
|
48
|
+
end
|
49
|
+
end
|
36
50
|
end
|