-
Notifications
You must be signed in to change notification settings - Fork 114
/
Copy pathvalidate_url.rb
95 lines (79 loc) · 3.65 KB
/
validate_url.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
require 'active_model'
require 'active_support/i18n'
require 'public_suffix'
I18n.load_path += Dir[File.dirname(__FILE__) + "/locale/*.yml"]
module ActiveModel
module Validations
class UrlValidator < ActiveModel::EachValidator
RESERVED_OPTIONS = [:schemes, :no_local]
def initialize(options)
options.reverse_merge!(schemes: %w(http https))
options.reverse_merge!(message: :url)
options.reverse_merge!(no_local: false)
options.reverse_merge!(public_suffix: false)
options.reverse_merge!(accept_array: false)
super(options)
end
def validate_each(record, attribute, value)
schemes = [*options.fetch(:schemes)].map(&:to_s)
if value.respond_to?(:each)
# Error out if we're not allowing arrays
if !options.include?(:accept_array) || !options.fetch(:accept_array)
record.errors.add(attribute, message, **filtered_options(value))
end
# We have to manually handle `:allow_nil` and `:allow_blank` since it's not caught by
# ActiveRecord's own validators. We do that by just removing all the nil's if we want to
# allow them so it's not passed on later.
value = value.reject(&:nil?) if options.include?(:allow_nil) && options.fetch(:allow_nil)
value = value.reject(&:blank?) if options.include?(:allow_blank) && options.fetch(:allow_blank)
result = value.flat_map { |v| validate_url(record, attribute, v, schemes) }
errors = result.reject(&:nil?)
return errors.any? ? errors.first : true
end
validate_url(record, attribute, value, schemes)
end
protected
def filtered_options(value)
filtered = options.except(*RESERVED_OPTIONS)
filtered[:value] = value
filtered
end
def validate_url(record, attribute, value, schemes)
uri = value && URI.parse(URI::Parser.new.escape(value))
host = uri && uri.host
scheme = uri && uri.scheme
valid_host = host && !host.empty?
valid_raw_url = scheme && value =~ /\A#{URI::regexp([scheme])}\z/
valid_scheme = host && scheme && schemes.include?(scheme)
valid_no_local = !options.fetch(:no_local) || (host && host.include?('.'))
valid_suffix = !options.fetch(:public_suffix) || (host && PublicSuffix.valid?(host, :default_rule => nil))
unless valid_host && valid_raw_url && valid_scheme && valid_no_local && valid_suffix
record.errors.add(attribute, message, value: value)
end
rescue URI::InvalidURIError, URI::InvalidComponentError
record.errors.add(attribute, message, **filtered_options(value))
end
def message
options.fetch(:message)
end
end
module ClassMethods
# Validates whether the value of the specified attribute is valid url.
#
# class Unicorn
# include ActiveModel::Validations
# attr_accessor :homepage, :ftpsite
# validates_url :homepage, allow_blank: true
# validates_url :ftpsite, schemes: ['ftp']
# end
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is not a valid URL").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:schemes</tt> - Array of URI schemes to validate against. (default is +['http', 'https']+)
def validates_url(*attr_names)
validates_with UrlValidator, _merge_attributes(attr_names)
end
end
end
end