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.
@@ -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
@@ -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 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/disposable.json", __dir__)
5
+ FILE_PATH = File.expand_path("../../data/disposable_domains.txt", __dir__)
6
6
 
7
7
  def self.all
8
- @all ||= JSON.parse(File.read(FILE_PATH))
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
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Validators
4
4
  class TLD
5
- FILE_PATH = File.expand_path("../../data/tld.json", __dir__)
5
+ FILE_PATH = File.expand_path("../../data/tld.txt", __dir__)
6
6
 
7
7
  def self.all
8
- @all ||= JSON.parse(File.read(FILE_PATH))
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 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::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
@@ -3,8 +3,8 @@
3
3
  module Validators
4
4
  module Version
5
5
  MAJOR = 3
6
- MINOR = 0
7
- PATCH = 5
6
+ MINOR = 3
7
+ PATCH = 0
8
8
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
9
9
  end
10
10
  end
@@ -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
@@ -1,16 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "simplecov"
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
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
11
- SimpleCov::Formatter::Console,
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
- DISPOSABLE_EMAILS = Validators::DisposableHostnames.all.sample(10)
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
- 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
 
21
22
  user = User.new(email: "user@custom.#{domain}")
22
23
  user.valid?
23
24
 
24
- assert_includes user.errors[:email], I18n.t("activerecord.errors.messages.disposable_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