validators 3.1.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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