validators 3.0.5 → 3.3.0

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