validators 3.1.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +11 -1
  5. data/README.md +49 -29
  6. data/Rakefile +0 -1
  7. data/data/reserved_subdomains.txt +2830 -0
  8. data/lib/validators.rb +12 -4
  9. data/lib/validators/constants.rb +1 -1
  10. data/lib/validators/disposable_domains.rb +18 -0
  11. data/lib/validators/disposable_emails.rb +22 -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 +7 -5
  16. data/lib/validators/validates_cnpj_format_of.rb +1 -2
  17. data/lib/validators/validates_cpf_format_of.rb +1 -2
  18. data/lib/validators/validates_email_format_of.rb +19 -4
  19. data/lib/validators/validates_ssh_private_key.rb +1 -3
  20. data/lib/validators/validates_ssh_public_key.rb +1 -3
  21. data/lib/validators/validates_subdomain.rb +69 -0
  22. data/lib/validators/validates_username.rb +15 -0
  23. data/lib/validators/version.rb +1 -1
  24. data/test/test_helper.rb +17 -2
  25. data/test/validators/disposable_email_test.rb +18 -5
  26. data/test/validators/validates_cnpj_format_of_test.rb +3 -3
  27. data/test/validators/validates_cpf_format_of_test.rb +3 -3
  28. data/test/validators/validates_email_format_of_test.rb +27 -2
  29. data/test/validators/validates_ssh_private_key/common_test.rb +3 -3
  30. data/test/validators/validates_ssh_public_key_test.rb +3 -3
  31. data/test/validators/validates_subdomain_test.rb +75 -0
  32. data/test/validators/validates_url_format_of/with_tld_validation_test.rb +18 -0
  33. data/test/validators/validates_url_format_of/without_tld_validation_test.rb +18 -0
  34. data/test/validators/{validates_reserved_username_test.rb → validates_username_test.rb} +27 -4
  35. data/validators.gemspec +11 -1
  36. metadata +116 -22
  37. data/bin/sync-disposable-hostnames +0 -35
  38. data/bin/sync-tld +0 -17
  39. data/data/disposable.json +0 -60403
  40. data/data/reserved_hostnames.json +0 -2836
  41. data/data/tld.json +0 -1516
  42. data/lib/validators/disposable_hostnames.rb +0 -11
  43. data/lib/validators/validates_reserved_hostname.rb +0 -45
  44. data/lib/validators/validates_reserved_username.rb +0 -29
  45. 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,15 @@ 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")]
28
+
29
+ def self.require_dependency!(dep)
30
+ require dep
31
+ rescue LoadError
32
+ raise "#{dep} is not part of the bundle. " \
33
+ "Add it to your project's Gemfile."
34
+ end
27
35
  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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validators
4
+ class DisposableDomains
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ Validators.require_dependency! "root_domain"
9
+ Validators.require_dependency! "email_data"
10
+ EmailData.disposable_domains
11
+ end
12
+ end
13
+
14
+ def self.include?(domain)
15
+ all.include?(domain)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validators
4
+ class DisposableEmails
5
+ def self.all
6
+ @all ||=
7
+ begin
8
+ Validators.require_dependency! "root_domain"
9
+ Validators.require_dependency! "email_data"
10
+ EmailData.disposable_emails
11
+ end
12
+ end
13
+
14
+ def self.include?(email)
15
+ mailbox, domain = email.to_s.split("@")
16
+ mailbox = mailbox.to_s.gsub(".", "")
17
+ mailbox = mailbox.gsub(/\+(.+)?\Z/, "")
18
+
19
+ all.include?("#{mailbox}@#{domain}")
20
+ end
21
+ end
22
+ 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,12 @@
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
+ Validators.require_dependency! "email_data"
9
+ EmailData.tlds
10
+ end
9
11
  end
10
12
 
11
13
  def self.host_with_valid_tld?(host)
@@ -13,10 +15,10 @@ module Validators
13
15
 
14
16
  return false if host.split(".").size == 1
15
17
 
16
- valid?(host[/\.([^.]+)$/, 1].to_s.downcase)
18
+ include?(host[/\.([^.]+)$/, 1].to_s.downcase)
17
19
  end
18
20
 
19
- def self.valid?(tld)
21
+ def self.include?(tld)
20
22
  all.include?(tld)
21
23
  end
22
24
  end
@@ -25,10 +25,9 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_cnpj_format_of(*attr_names)
28
+ Validators.require_dependency! "cpf_cnpj"
28
29
  require "cnpj"
29
30
  validates_with CnpjValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "cpf_cnpj is not part of the bundle. Add it to Gemfile."
32
31
  end
33
32
 
34
33
  alias_method :validates_cnpj, :validates_cnpj_format_of
@@ -25,10 +25,9 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_cpf_format_of(*attr_names)
28
+ Validators.require_dependency! "cpf_cnpj"
28
29
  require "cpf"
29
30
  validates_with CpfValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "cpf_cnpj is not part of the bundle. Add it to Gemfile."
32
31
  end
33
32
 
34
33
  alias_method :validates_cpf, :validates_cpf_format_of
@@ -14,6 +14,7 @@ module ActiveModel
14
14
 
15
15
  validate_tld(record, attribute, value, options) if check_tld
16
16
  validate_email_format(record, attribute, value, options)
17
+ validate_disposable_domain(record, attribute, value, options) unless allow_disposable
17
18
  validate_disposable_email(record, attribute, value, options) unless allow_disposable
18
19
  end
19
20
 
@@ -41,12 +42,24 @@ module ActiveModel
41
42
  )
42
43
  end
43
44
 
44
- def validate_disposable_email(record, attribute, value, _options)
45
+ def validate_disposable_domain(record, attribute, value, _options)
46
+ return unless value
47
+
45
48
  hostname = value.to_s.split(AT_SIGN).last.to_s.downcase
49
+ root_domain = RootDomain.call(hostname)
50
+
51
+ return unless Validators::DisposableDomains.include?(root_domain)
52
+
53
+ record.errors.add(
54
+ attribute,
55
+ :disposable_domain,
56
+ value: value
57
+ )
58
+ end
46
59
 
47
- return if Validators::DisposableHostnames.all.none? do |disposable_hostname|
48
- hostname == disposable_hostname || hostname.end_with?(".#{disposable_hostname}")
49
- end
60
+ def validate_disposable_email(record, attribute, value, _options)
61
+ return unless value
62
+ return unless Validators::DisposableEmails.include?(value)
50
63
 
51
64
  record.errors.add(
52
65
  attribute,
@@ -64,6 +77,8 @@ module ActiveModel
64
77
  # end
65
78
  #
66
79
  def validates_email_format_of(*attr_names)
80
+ Validators.require_dependency! "root_domain"
81
+ Validators.require_dependency! "email_data"
67
82
  validates_with EmailValidator, _merge_attributes(attr_names)
68
83
  end
69
84
 
@@ -56,10 +56,8 @@ module ActiveModel
56
56
  # end
57
57
  #
58
58
  def validates_ssh_private_key(*attr_names)
59
- require "sshkey"
59
+ Validators.require_dependency! "sshkey"
60
60
  validates_with SshPrivateKeyValidator, _merge_attributes(attr_names)
61
- rescue LoadError
62
- raise "sshkey is not part of the bundle. Add it to Gemfile."
63
61
  end
64
62
  end
65
63
  end
@@ -25,10 +25,8 @@ module ActiveModel
25
25
  # end
26
26
  #
27
27
  def validates_ssh_public_key(*attr_names)
28
- require "sshkey"
28
+ Validators.require_dependency! "sshkey"
29
29
  validates_with SshPublicKeyValidator, _merge_attributes(attr_names)
30
- rescue LoadError
31
- raise "sshkey is not part of the bundle. Add it to Gemfile."
32
30
  end
33
31
  end
34
32
  end
@@ -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 = 1
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
@@ -15,10 +15,10 @@ class ValidatesCnpjFormatOfTest < Minitest::Test
15
15
  end
16
16
  end
17
17
 
18
- test "fails when gem is not available" do
19
- assert_raises do
18
+ test "fails when cpf_cnpj is not available" do
19
+ assert_raises(StandardError, /cpf_cnpj is not part of the bundle/) do
20
20
  Class.new do
21
- expects(:require).with("cnpj").raises(LoadError)
21
+ Validators.expects(:require).with("cpf_cnpj").raises(LoadError, "-- cpf_cnpj")
22
22
 
23
23
  include ActiveModel::Model
24
24
  validates_cnpj_format_of :document