validators 3.1.0 → 3.4.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +10 -0
  5. data/README.md +23 -8
  6. data/Rakefile +0 -1
  7. data/data/reserved_subdomains.txt +2830 -0
  8. data/lib/validators.rb +5 -4
  9. data/lib/validators/constants.rb +1 -1
  10. data/lib/validators/disposable_domains.rb +19 -0
  11. data/lib/validators/disposable_emails.rb +23 -0
  12. data/lib/validators/locale/en.yml +5 -2
  13. data/lib/validators/locale/pt-BR.yml +1 -0
  14. data/lib/validators/{reserved_hostnames.rb → reserved_subdomains.rb} +3 -5
  15. data/lib/validators/tld.rb +9 -5
  16. data/lib/validators/validates_email_format_of.rb +19 -4
  17. data/lib/validators/validates_subdomain.rb +69 -0
  18. data/lib/validators/validates_username.rb +15 -0
  19. data/lib/validators/version.rb +1 -1
  20. data/test/test_helper.rb +17 -2
  21. data/test/validators/disposable_email_test.rb +18 -5
  22. data/test/validators/validates_email_format_of_test.rb +4 -2
  23. data/test/validators/validates_subdomain_test.rb +75 -0
  24. data/test/validators/validates_url_format_of/with_tld_validation_test.rb +18 -0
  25. data/test/validators/validates_url_format_of/without_tld_validation_test.rb +18 -0
  26. data/test/validators/{validates_reserved_username_test.rb → validates_username_test.rb} +27 -4
  27. data/validators.gemspec +11 -1
  28. metadata +116 -22
  29. data/bin/sync-disposable-hostnames +0 -35
  30. data/bin/sync-tld +0 -17
  31. data/data/disposable.json +0 -57281
  32. data/data/reserved_hostnames.json +0 -1399
  33. data/data/tld.json +0 -1516
  34. data/lib/validators/disposable_hostnames.rb +0 -11
  35. data/lib/validators/validates_reserved_hostname.rb +0 -45
  36. data/lib/validators/validates_reserved_username.rb +0 -29
  37. data/test/validators/validates_reserved_hostname_test.rb +0 -40
@@ -7,8 +7,9 @@ module Validators
7
7
  require "validators/ip"
8
8
  require "validators/tld"
9
9
  require "validators/hostname"
10
- require "validators/disposable_hostnames"
11
- require "validators/reserved_hostnames"
10
+ require "validators/disposable_domains"
11
+ require "validators/disposable_emails"
12
+ require "validators/reserved_subdomains"
12
13
 
13
14
  require "validators/validates_datetime"
14
15
  require "validators/validates_ip_address"
@@ -20,8 +21,8 @@ module Validators
20
21
  require "validators/validates_ssh_private_key"
21
22
  require "validators/validates_ssh_public_key"
22
23
  require "validators/validates_hostname_format_of"
23
- require "validators/validates_reserved_hostname"
24
- require "validators/validates_reserved_username"
24
+ require "validators/validates_subdomain"
25
+ require "validators/validates_username"
25
26
 
26
27
  I18n.load_path += Dir[File.join(__dir__, "validators/locale/*.yml")]
27
28
  end
@@ -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[\w][\w\d._-]*[\w\d_-]+(\+[\w\d]+)?@(hotmail|outlook).com\z/i.freeze
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 DisposableDomains
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ require "email_data"
9
+ EmailData.disposable_domains
10
+ rescue LoadError
11
+ raise "email_data is not part of the bundle. Add it to Gemfile."
12
+ end
13
+ end
14
+
15
+ def self.include?(domain)
16
+ all.include?(domain)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validators
4
+ class DisposableEmails
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ require "email_data"
9
+ EmailData.disposable_emails
10
+ rescue LoadError
11
+ raise "email_data is not part of the bundle. Add it to Gemfile."
12
+ end
13
+ end
14
+
15
+ def self.include?(email)
16
+ mailbox, domain = email.to_s.split("@")
17
+ mailbox = mailbox.to_s.gsub(".", "")
18
+ mailbox = mailbox.gsub(/\+(.+)?\Z/, "")
19
+
20
+ all.include?("#{mailbox}@#{domain}")
21
+ end
22
+ end
23
+ end
@@ -3,7 +3,8 @@ en:
3
3
  activemodel: &activemodel
4
4
  errors:
5
5
  messages:
6
- disposable_email: "is not allowed (high-bounce domain)"
6
+ disposable_email: "is not allowed (high-bounce email)"
7
+ disposable_domain: "is not allowed (high-bounce domain)"
7
8
  invalid_cnpj: "is not a valid CNPJ"
8
9
  invalid_cpf: "is not a valid CPF"
9
10
  invalid_date: "is not a valid date"
@@ -20,8 +21,10 @@ en:
20
21
  invalid_ssh_private_key_type: "must be a %{value} key"
21
22
  invalid_ssh_public_key: "is not a valid public SSH key"
22
23
  invalid_url: "is not a valid address"
23
- reserved_hostname: "%{value} is a reserved hostname"
24
+ reserved_subdomain: "%{value} is a reserved subdomain"
25
+ invalid_subdomain: "is invalid"
24
26
  reserved_username: "%{value} is a reserved username"
27
+ invalid_username: "is invalid"
25
28
 
26
29
  activerecord:
27
30
  <<: *activemodel
@@ -4,6 +4,7 @@ pt-BR:
4
4
  errors:
5
5
  messages:
6
6
  disposable_email: "não é permitido (e-mail temporário)"
7
+ disposable_domain: "não é permitido (e-mail temporário)"
7
8
  invalid_cnpj: "não é um CNPJ válido"
8
9
  invalid_cpf: "não é um CPF válido"
9
10
  invalid_date: "não é uma data válida"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Validators
4
- class ReservedHostnames
5
- FILE_PATH = File.expand_path("../../data/reserved_hostnames.json", __dir__)
4
+ class ReservedSubdomains
5
+ FILE_PATH = File.expand_path("../../data/reserved_subdomains.txt", __dir__)
6
6
 
7
7
  def self.reserved?(hostname, matchers = nil)
8
8
  matchers = parse_list(matchers) if matchers
@@ -11,9 +11,7 @@ module Validators
11
11
  end
12
12
 
13
13
  def self.all
14
- @all ||= JSON
15
- .parse(File.read(FILE_PATH))
16
- .map {|matcher| parse(matcher) }
14
+ @all ||= File.read(FILE_PATH).lines.map {|matcher| parse(matcher.chomp) }
17
15
  end
18
16
 
19
17
  def self.parse(matcher)
@@ -2,10 +2,14 @@
2
2
 
3
3
  module Validators
4
4
  class TLD
5
- FILE_PATH = File.expand_path("../../data/tld.json", __dir__)
6
-
7
5
  def self.all
8
- @all ||= JSON.parse(File.read(FILE_PATH))
6
+ @all ||=
7
+ begin
8
+ require "email_data"
9
+ EmailData.tlds
10
+ rescue LoadError
11
+ raise "email_data is not part of the bundle. Add it to Gemfile."
12
+ end
9
13
  end
10
14
 
11
15
  def self.host_with_valid_tld?(host)
@@ -13,10 +17,10 @@ module Validators
13
17
 
14
18
  return false if host.split(".").size == 1
15
19
 
16
- valid?(host[/\.([^.]+)$/, 1].to_s.downcase)
20
+ include?(host[/\.([^.]+)$/, 1].to_s.downcase)
17
21
  end
18
22
 
19
- def self.valid?(tld)
23
+ def self.include?(tld)
20
24
  all.include?(tld)
21
25
  end
22
26
  end
@@ -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 validate_disposable_email(record, attribute, value, _options)
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 if Validators::DisposableHostnames.all.none? do |disposable_hostname|
48
- hostname == disposable_hostname || hostname.end_with?(".#{disposable_hostname}")
49
- end
53
+ return unless Validators::DisposableDomains.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
@@ -3,7 +3,7 @@
3
3
  module Validators
4
4
  module Version
5
5
  MAJOR = 3
6
- MINOR = 1
6
+ MINOR = 4
7
7
  PATCH = 0
8
8
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
9
9
  end
@@ -3,7 +3,6 @@
3
3
  $VERBOSE = nil
4
4
 
5
5
  require "simplecov"
6
- SimpleCov.start
7
6
 
8
7
  SimpleCov.start do
9
8
  add_filter "test/support"
@@ -17,9 +16,25 @@ require "active_support/all"
17
16
  require "minitest/utils"
18
17
  require "minitest/autorun"
19
18
 
19
+ def build_email_with_filter(email)
20
+ mailbox, domain = email.split("@")
21
+ "#{mailbox}+#{SecureRandom.hex(3)}@#{domain}"
22
+ end
23
+
24
+ def build_email_with_dots(email)
25
+ mailbox, domain = email.split("@")
26
+ new_mailbox = mailbox.chars.map {|char| [char, "."] }.flatten[0..-2].join
27
+
28
+ "#{new_mailbox}@#{domain}"
29
+ end
30
+
20
31
  Time.zone = "America/Sao_Paulo"
21
32
  TLDs = Validators::TLD.all.sample(10)
22
- DISPOSABLE_EMAILS = Validators::DisposableHostnames.all.sample(10)
33
+ DISPOSABLE_DOMAINS = Validators::DisposableDomains.all.sample(10)
34
+ DISPOSABLE_EMAILS = Validators::DisposableEmails.all +
35
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(email) } +
36
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_dots(email) } +
37
+ Validators::DisposableEmails.all.sample(10).map {|email| build_email_with_filter(build_email_with_dots(email)) }
23
38
 
24
39
  Dir[File.join(__dir__, "support/**/*.rb")].sort.each {|f| require f }
25
40
 
@@ -3,18 +3,19 @@
3
3
  require "test_helper"
4
4
 
5
5
  class DisposableEmailTest < Minitest::Test
6
- DISPOSABLE_EMAILS.each do |domain|
7
- test "rejects disposable e-mail (#{domain})" do
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], I18n.t("activerecord.errors.messages.disposable_email")
13
+ assert_includes user.errors[:email],
14
+ I18n.t("activerecord.errors.messages.disposable_domain")
14
15
  end
15
16
  end
16
17
 
17
- DISPOSABLE_EMAILS.each do |domain|
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
 
@@ -22,7 +23,7 @@ class DisposableEmailTest < Minitest::Test
22
23
  user.valid?
23
24
 
24
25
  assert_includes user.errors[:email],
25
- "is not allowed (high-bounce domain)"
26
+ I18n.t("activerecord.errors.messages.disposable_domain")
26
27
  end
27
28
  end
28
29
 
@@ -34,4 +35,16 @@ class DisposableEmailTest < Minitest::Test
34
35
 
35
36
  assert user.errors[:email].empty?
36
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
37
50
  end
@@ -12,10 +12,12 @@ class ValidatesEmailFormatOfTest < Minitest::Test
12
12
  VALID_EMAILS.each do |email|
13
13
  test "accepts #{email.inspect} as a valid email" do
14
14
  user = User.new(email: email, corporate_email: email)
15
- assert user.valid?
15
+ user.valid?
16
+ assert_empty user.errors
16
17
 
17
18
  user = Person.new(email: email)
18
- assert user.valid?
19
+ user.valid?
20
+ assert_empty user.errors
19
21
  end
20
22
  end
21
23
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ValidatesSubdomainTest < Minitest::Test
6
+ test "don't explode with nil values" do
7
+ model = build_model do
8
+ attr_accessor :subdomain
9
+ validates_subdomain :subdomain
10
+ end
11
+
12
+ instance = model.new(subdomain: nil)
13
+
14
+ refute instance.valid?
15
+ assert_includes instance.errors[:subdomain], "is invalid"
16
+ end
17
+
18
+ test "rejects invalid subdomain" do
19
+ model = build_model do
20
+ attr_accessor :subdomain
21
+ validates_subdomain :subdomain
22
+ end
23
+
24
+ instance = model.new(subdomain: "1234")
25
+
26
+ refute instance.valid?
27
+ assert_includes instance.errors[:subdomain], "is invalid"
28
+ end
29
+
30
+ test "rejects reserved subdomain" do
31
+ model = build_model do
32
+ attr_accessor :subdomain
33
+ validates_subdomain :subdomain
34
+ end
35
+
36
+ instance = model.new(subdomain: "www")
37
+
38
+ refute instance.valid?
39
+ assert_includes instance.errors[:subdomain],
40
+ "www is a reserved subdomain"
41
+ end
42
+
43
+ test "rejects reserved subdomain with pattern" do
44
+ model = build_model do
45
+ attr_accessor :subdomain
46
+ validates_subdomain :subdomain
47
+ end
48
+
49
+ instance = model.new(subdomain: "www1234")
50
+
51
+ refute instance.valid?
52
+ end
53
+
54
+ test "uses custom list" do
55
+ model = build_model do
56
+ attr_accessor :subdomain
57
+ validates_subdomain :subdomain, in: %w[nope]
58
+ end
59
+
60
+ instance = model.new(subdomain: "nope")
61
+
62
+ refute instance.valid?
63
+ end
64
+
65
+ test "ignores reserved subdomain validation" do
66
+ model = build_model do
67
+ attr_accessor :subdomain
68
+ validates_subdomain :subdomain, reserved: false
69
+ end
70
+
71
+ instance = model.new(subdomain: "www")
72
+
73
+ assert instance.valid?
74
+ end
75
+ end