Rails: Какой хороший способ проверять ссылки (URL)?

125

Мне было интересно, как лучше всего проверять URL-адреса в Rails. Я думал об использовании регулярного выражения, но не уверен, что это лучший способ.

И, если бы я использовал регулярное выражение, мог бы кто-нибудь предложить мне его? Я все еще новичок в Regex.

сойка
источник
Связанный: stackoverflow.com/questions/1805761/…
Джон Шнайдер

Ответы:

151

Проверка URL - непростая задача. Это тоже очень широкий запрос.

Что именно вы хотите делать? Вы хотите проверить формат URL, наличие или что-то еще? Есть несколько возможностей, в зависимости от того, что вы хотите сделать.

Регулярное выражение может проверять формат URL-адреса. Но даже сложное регулярное выражение не может гарантировать, что вы имеете дело с действительным URL.

Например, если вы возьмете простое регулярное выражение, оно, вероятно, отклонит следующий хост

http://invalid##host.com

но это позволит

http://invalid-host.foo

это действительный хост, но не действительный домен, если учесть существующие TLD. Действительно, решение будет работать, если вы хотите проверить имя хоста, а не домен, потому что следующее является допустимым именем хоста

http://host.foo

а также следующий

http://localhost

Теперь позвольте мне предложить вам несколько решений.

Если вы хотите проверить домен, вам нужно забыть о регулярных выражениях. Лучшее решение, доступное на данный момент, - это список общедоступных суффиксов, который поддерживается Mozilla. Я создал библиотеку Ruby для анализа и проверки доменов на соответствие списку публичных суффиксов, и она называется PublicSuffix. .

Если вы хотите проверить формат URI / URL-адреса, вы можете использовать регулярные выражения. Вместо поиска используйте встроенный URI.parseметод Ruby .

require 'uri'

def valid_url?(uri)
  uri = URI.parse(uri) && !uri.host.nil?
rescue URI::InvalidURIError
  false
end

Вы даже можете сделать его более строгим. Например, если вы хотите, чтобы URL-адрес был URL-адресом HTTP / HTTPS, вы можете сделать проверку более точной.

require 'uri'

def valid_url?(url)
  uri = URI.parse(url)
  uri.is_a?(URI::HTTP) && !uri.host.nil?
rescue URI::InvalidURIError
  false
end

Конечно, к этому методу можно применить массу улучшений, включая проверку пути или схемы.

И последнее, но не менее важное: вы также можете упаковать этот код в валидатор:

class HttpUrlValidator < ActiveModel::EachValidator

  def self.compliant?(value)
    uri = URI.parse(value)
    uri.is_a?(URI::HTTP) && !uri.host.nil?
  rescue URI::InvalidURIError
    false
  end

  def validate_each(record, attribute, value)
    unless value.present? && self.class.compliant?(value)
      record.errors.add(attribute, "is not a valid HTTP URL")
    end
  end

end

# in the model
validates :example_attribute, http_url: true
Симоне Карлетти
источник
1
Обратите внимание, что класс будет URI::HTTPSдля https uris (например:URI.parse("https://yo.com").class => URI::HTTPS
tee
12
URI::HTTPSнаследуется от URI:HTTP, поэтому я использую kind_of?.
Simone Carletti
1
Безусловно, наиболее полное решение для безопасной проверки URL-адреса.
Фабрицио Регини
4
URI.parse('http://invalid-host.foo')возвращает истину, потому что этот URI является допустимым URL. Также обратите внимание, что .fooтеперь это действующий TLD. iana.org/domains/root/db/foo.html
Симоне Карлетти,
1
@jmccartie, пожалуйста, прочтите весь пост. Если вам важна схема, вы должны использовать окончательный код, который также включает проверку типа, а не только эту строку. Вы перестали читать до конца поста.
Симоне Карлетти
101

Внутри моих моделей я использую одинарный лайнер:

validates :url, format: URI::regexp(%w[http https])

Думаю, достаточно хорош и прост в использовании. Более того, теоретически он должен быть эквивалентен методу Симона, поскольку внутри он использует то же регулярное выражение.

Маттео Коллина
источник
17
К сожалению, 'http://'соответствует приведенному выше шаблону. См .:URI::regexp(%w(http https)) =~ 'http://'
Дэвид Дж.
15
Также http:fakeбудет действительным URL-адрес .
nathanvda
54

Следуя идее Симоны, вы легко можете создать собственный валидатор.

class UrlValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?
    begin
      uri = URI.parse(value)
      resp = uri.kind_of?(URI::HTTP)
    rescue URI::InvalidURIError
      resp = false
    end
    unless resp == true
      record.errors[attribute] << (options[:message] || "is not an url")
    end
  end
end

а затем используйте

validates :url, :presence => true, :url => true

в вашей модели.

jlfenaux
источник
1
где мне поставить этот класс? В инициализаторе?
deb
3
Я цитирую @gbc: «Если вы разместите свои собственные валидаторы в app / validators, они будут автоматически загружены без необходимости изменять ваш файл config / application.rb». ( stackoverflow.com/a/6610270/839847 ). Обратите внимание, что ответ Стефана Петтерссона ниже показывает, что он также сохранил аналогичный файл в «приложении / валидаторах».
bergie3000
4
это только проверяет, начинается ли URL с http: // или https: //, это неправильная проверка URL
maggix
1
Завершите, если вы можете позволить URL-адрес быть необязательным: class OptionalUrlValidator <UrlValidator def validate_each (record, attribute, value) return true if value.blank? return super end end
Dirty Henry
1
Это не очень хорошая проверка:URI("http:").kind_of?(URI::HTTP) #=> true
smathy
29

Также есть гем validate_url (который является просто хорошей оболочкой для Addressable::URI.parseрешения).

Просто добавь

gem 'validate_url'

к вашему Gemfile, а затем в моделях вы можете

validates :click_through_url, url: true
П. Долженко
источник
@ ЕвгенийМасленков, это может быть так же хорошо, потому что он действителен согласно спецификации, но вы можете проверить github.com/sporkmonger/addressable/issues . Также в общем случае мы обнаружили, что никто не следует стандарту и вместо этого использует простую проверку формата.
dolzenko
13

На этот вопрос уже дан ответ, но, черт возьми, я предлагаю решение, которое использую.

Регулярное выражение отлично работает со всеми URL-адресами, которые я встречал. Метод установки должен позаботиться о том, чтобы протокол не упоминался (предположим, http: //).

И, наконец, мы делаем попытку получить страницу. Возможно, мне следует принимать перенаправления, а не только HTTP 200 OK.

# app/models/my_model.rb
validates :website, :allow_blank => true, :uri => { :format => /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix }

def website= url_str
  unless url_str.blank?
    unless url_str.split(':')[0] == 'http' || url_str.split(':')[0] == 'https'
        url_str = "http://" + url_str
    end
  end  
  write_attribute :website, url_str
end

и...

# app/validators/uri_vaidator.rb
require 'net/http'

# Thanks Ilya! http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/
# Original credits: http://blog.inquirylabs.com/2006/04/13/simple-uri-validation/
# HTTP Codes: http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/classes/Net/HTTPResponse.html

class UriValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    raise(ArgumentError, "A regular expression must be supplied as the :format option of the options hash") unless options[:format].nil? or options[:format].is_a?(Regexp)
    configuration = { :message => I18n.t('errors.events.invalid_url'), :format => URI::regexp(%w(http https)) }
    configuration.update(options)

    if value =~ configuration[:format]
      begin # check header response
        case Net::HTTP.get_response(URI.parse(value))
          when Net::HTTPSuccess then true
          else object.errors.add(attribute, configuration[:message]) and false
        end
      rescue # Recover on DNS failures..
        object.errors.add(attribute, configuration[:message]) and false
      end
    else
      object.errors.add(attribute, configuration[:message]) and false
    end
  end
end
Стефан Петтерссон
источник
действительно аккуратно! спасибо за ваш вклад, часто есть много подходов к проблеме; это здорово, когда люди делятся своим.
jay
6
Просто хотел указать, что в соответствии с руководством по безопасности rails вы должны использовать \ A и \ z, а не $ ^ в этом регулярном выражении
Джаред
1
Мне это нравится. Быстрое предложение немного высушить код, переместив регулярное выражение в валидатор, поскольку я полагаю, вы хотите, чтобы он был согласован между моделями. Бонус: это позволит вам опустить первую строку под validate_each.
Пол Петтенгилл
Что делать, если URL-адрес занимает много времени и истекает время ожидания? Что будет лучшим вариантом для отображения сообщения об ошибке тайм-аута или при невозможности открытия страницы?
user588324
это никогда не пройдет аудит безопасности, вы заставляете свои серверы тыкать произвольный URL
Маурисио
12

Вы также можете попробовать valid_url гем который разрешает URL-адреса без схемы, проверяет доменную зону и имена IP-хостов.

Добавьте его в свой Gemfile:

gem 'valid_url'

А затем в модели:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Роман Раловец
источник
Это так приятно, особенно URL-адреса без схемы, которые неожиданно связаны с классом URI.
Пол Петтенгилл
Я был удивлен способностью этого драгоценного камня копаться в IP-адресах и обнаруживать поддельные. Спасибо!
Whiz of Oz
10

Только мои 2 цента:

before_validation :format_website
validate :website_validator

private

def format_website
  self.website = "http://#{self.website}" unless self.website[/^https?/]
end

def website_validator
  errors[:website] << I18n.t("activerecord.errors.messages.invalid") unless website_valid?
end

def website_valid?
  !!website.match(/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-=\?]*)*\/?$/)
end

EDIT: изменено регулярное выражение для соответствия URL-адресам параметров.

Lafeber
источник
1
спасибо за ваш вклад, всегда приятно видеть разные решения
джей
Кстати, ваше регулярное выражение будет отклонять действительные URL-адреса со строкой запроса, такой какhttp://test.com/fdsfsdf?a=b
MikDiet
2
Мы запустили этот код в производство и продолжали получать тайм-ауты в бесконечных циклах в строке регулярного выражения .match. Не уверен, почему, просто предостерегаем некоторых случаев, и хотелось бы услышать мысли других о том, почему это могло произойти.
toobulkeh
10

Решение, которое сработало для меня, было:

validates_format_of :url, :with => /\A(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w\.-]*)*\/?\Z/i

Я попытался использовать некоторые из примеров, которые вы прикрепили, но я поддерживаю URL-адрес следующим образом:

Обратите внимание на использование A и Z, потому что если вы используете ^ и $, вы увидите это предупреждение безопасности от валидаторов Rails.

 Valid ones:
 'www.crowdint.com'
 'crowdint.com'
 'http://crowdint.com'
 'http://www.crowdint.com'

 Invalid ones:
  'http://www.crowdint. com'
  'http://fake'
  'http:fake'
Эриберто Перес
источник
1
Попробуйте это с помощью "https://portal.example.com/portal/#". В Ruby 2.1.6 оценка зависает.
Old Pro
вы правы, похоже, что в некоторых случаях это регулярное выражение требует
вечности
1
очевидно, что не существует регулярного выражения, охватывающего все сценарии, поэтому я использую простую проверку: validates: url, format: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
heriberto perez
5

В последнее время я столкнулся с той же проблемой (мне нужно было проверить URL-адреса в приложении Rails), но мне пришлось справиться с дополнительным требованием URL-адресов Unicode (например, http://кц.рф ) ...

Я исследовал несколько решений и наткнулся на следующее:

  • Первый и наиболее рекомендуемый вариант - использовать URI.parse . Подробности читайте в ответе Симоне Карлетти. Это работает нормально, но не для URL-адресов Unicode.
  • Второй метод, который я видел, был разработан Ильей Григориком: http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/ По сути, он пытается сделать запрос к URL; если работает, то действительно ...
  • Третий метод, который я нашел (и тот, который я предпочитаю), похож на подход, URI.parseно с использованием addressableдрагоценного камня вместо URIstdlib. Этот подход подробно описан здесь: http://rawsyntax.com/blog/url-validation-in-rails-3-and-ruby-in-general/
Северин
источник
Да, но Addressable::URI.parse('http:///').scheme # => "http"or Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')- совершенно нормально с точки зрения Addressable :(
smileart
4

Вот обновленная версия валидатора, опубликованная Дэвидом Джеймсом . Его опубликовал Бенджамин Флейшер . Тем временем я выпустил обновленную вилку, которую можно найти здесь .

require 'addressable/uri'

# Source: http://gist.github.com/bf4/5320847
# Accepts options[:message] and options[:allowed_protocols]
# spec/validators/uri_validator_spec.rb
class UriValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    uri = parse_uri(value)
    if !uri
      record.errors[attribute] << generic_failure_message
    elsif !allowed_protocols.include?(uri.scheme)
      record.errors[attribute] << "must begin with #{allowed_protocols_humanized}"
    end
  end

private

  def generic_failure_message
    options[:message] || "is an invalid URL"
  end

  def allowed_protocols_humanized
    allowed_protocols.to_sentence(:two_words_connector => ' or ')
  end

  def allowed_protocols
    @allowed_protocols ||= [(options[:allowed_protocols] || ['http', 'https'])].flatten
  end

  def parse_uri(value)
    uri = Addressable::URI.parse(value)
    uri.scheme && uri.host && uri
  rescue URI::InvalidURIError, Addressable::URI::InvalidURIError, TypeError
  end

end

...

require 'spec_helper'

# Source: http://gist.github.com/bf4/5320847
# spec/validators/uri_validator_spec.rb
describe UriValidator do
  subject do
    Class.new do
      include ActiveModel::Validations
      attr_accessor :url
      validates :url, uri: true
    end.new
  end

  it "should be valid for a valid http url" do
    subject.url = 'http://www.google.com'
    subject.valid?
    subject.errors.full_messages.should == []
  end

  ['http://google', 'http://.com', 'http://ftp://ftp.google.com', 'http://ssh://google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is a invalid http url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.full_messages.should == []
    end
  end

  ['http:/www.google.com','<>hi'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("is an invalid URL")
    end
  end

  ['www.google.com','google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("is an invalid URL")
    end
  end

  ['ftp://ftp.google.com','ssh://google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("must begin with http or https")
    end
  end
end

Обратите внимание, что все еще существуют странные HTTP-URI, которые анализируются как действительные адреса.

http://google  
http://.com  
http://ftp://ftp.google.com  
http://ssh://google.com

Вот проблема для addressableдрагоценного камня, который охватывает примеры.

JJD
источник
3

Я использую небольшой вариант решения Lafeber, описанного выше . Он запрещает использование следующих друг за другом точек в имени хоста (например, в www.many...dots.com):

%r"\A(https?://)?[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]{2,6}(/.*)?\Z"i

URI.parseпохоже, требует префикса схемы, что в некоторых случаях не то, что вам может понадобиться (например, если вы хотите разрешить своим пользователям быстро писать URL-адреса в таких формах, как twitter.com/username)

Франко
источник
2

Я использую гем activevalidators, и он работает очень хорошо (не только для проверки URL-адресов)

Вы можете найти это здесь

Все это задокументировано, но в основном после добавления драгоценного камня вы захотите добавить следующие несколько строк в инициализатор, например: /config/environments/initializers/active_validators_activation.rb

# Activate all the validators
ActiveValidators.activate(:all)

(Примечание: вы можете заменить: all на: url или: что угодно, если вы просто хотите проверить определенные типы значений)

А затем вернемся к вашей модели примерно так

class Url < ActiveRecord::Base
   validates :url, :presence => true, :url => true
end

Теперь перезапустите сервер, и все должно быть

Арно Бушо
источник
2

Если вам нужна простая проверка и пользовательское сообщение об ошибке:

  validates :some_field_expecting_url_value,
            format: {
              with: URI.regexp(%w[http https]),
              message: 'is not a valid URL'
            }
Калеб
источник
1

Вы можете проверить несколько URL-адресов, используя что-то вроде:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Дэмиен Рош
источник
1
Как бы вы обрабатывали URL без схемы (например, www.bar.com/foo)?
Craig 09
1

Недавно у меня была такая же проблема, и я нашел способ обойти действительные URL-адреса.

validates_format_of :url, :with => URI::regexp(%w(http https))
validate :validate_url
def validate_url

  unless self.url.blank?

    begin

      source = URI.parse(self.url)

      resp = Net::HTTP.get_response(source)

    rescue URI::InvalidURIError

      errors.add(:url,'is Invalid')

    rescue SocketError 

      errors.add(:url,'is Invalid')

    end



  end

Первой части метода validate_url достаточно для проверки формата URL. Вторая часть будет проверять, существует ли URL-адрес, отправив запрос.

Dilnavaz
источник
Что, если URL-адрес указывает на очень большой ресурс (скажем, несколько гигабайт)?
Джон Шнайдер
@JonSchneider можно использовать HTTP-запрос заголовка (как здесь ) вместо get.
wvengen
1

Мне понравился monkeypatch модуль URI, чтобы добавить действительный? метод

внутри config/initializers/uri.rb

module URI
  def self.valid?(url)
    uri = URI.parse(url)
    uri.is_a?(URI::HTTP) && !uri.host.nil?
  rescue URI::InvalidURIError
    false
  end
end
Блэр Андерсон
источник
0

И как модуль

module UrlValidator
  extend ActiveSupport::Concern
  included do
    validates :url, presence: true, uniqueness: true
    validate :url_format
  end

  def url_format
    begin
      errors.add(:url, "Invalid url") unless URI(self.url).is_a?(URI::HTTP)
    rescue URI::InvalidURIError
      errors.add(:url, "Invalid url")
    end
  end
end

И затем просто include UrlValidatorв любой модели, для которой вы хотите проверить URL-адрес. Просто в том числе для опций.

MCB
источник
0

Проверка URL-адресов не может быть выполнена просто с помощью регулярного выражения, поскольку количество веб-сайтов продолжает расти, а новые схемы именования доменов продолжают появляться.

В моем случае я просто пишу собственный валидатор, который проверяет успешный ответ.

class UrlValidator < ActiveModel::Validator
  def validate(record)
    begin
      url = URI.parse(record.path)
      response = Net::HTTP.get(url)
      true if response.is_a?(Net::HTTPSuccess)   
    rescue StandardError => error
      record.errors[:path] << 'Web address is invalid'
      false
    end  
  end
end

Я проверяю pathатрибут моей модели, используя record.path. Я также помещаю ошибку в соответствующее имя атрибута, используяrecord.errors[:path] .

Вы можете просто заменить его любым именем атрибута.

Затем я просто вызываю пользовательский валидатор в своей модели.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Номан Ур Рехман
источник
Что, если URL-адрес указывает на очень большой ресурс (скажем, несколько гигабайт)?
Джон Шнайдер
0

Вы можете использовать для этого регулярное выражение, для меня это хорошо работает:

(^|[\s.:;?\-\]<\(])(ftp|https?:\/\/[-\w;\/?:@&=+$\|\_.!~*\|'()\[\]%#,]+[\w\/#](\(\))?)(?=$|[\s',\|\(\).:;?\-\[\]>\)])
spirito_libero
источник