Rails: Який хороший спосіб перевірити посилання (URL-адреси)?


125

Мені було цікаво, як мені найкраще перевірити URL-адреси в Rails. Я думав використовувати регулярний вираз, але не впевнений, чи це найкраща практика.

І якщо я буду використовувати регулярний вираз, чи міг би хтось запропонувати мені його? Я все ще новачок у Regex.


Відповіді:


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 Юріс (наприклад:URI.parse("https://yo.com").class => URI::HTTPS
трійник

12
URI::HTTPSуспадковує від URI:HTTPцього, тому я і використовую kind_of?.
Сімоне Карлетті

1
На сьогодні найповніше рішення для безпечної перевірки URL-адреси.
Фабріціо Регіні

4
URI.parse('http://invalid-host.foo')повертає true, тому що 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
Також URL-адреса на зразок http:fakeбуде дійсною.
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

у вашій моделі.


1
куди мені поставити цей клас? В ініціалізаторі?
дебют

3
Я цитую від @gbc: "Якщо ви розмістите свої власні валідатори у програмі / валідаторах, вони автоматично завантажуються без необхідності змінювати файл config / application.rb." ( stackoverflow.com/a/6610270/839847 ). Зауважте, що відповідь нижче від Стефана Петтерссона показує, що він зберег подібний файл також у "застосунку / валідатори".
bergie3000

4
це перевіряє, чи URL-адреса починається з http: // або https: //, це не правильна перевірка URL-адреси
maggix

1
Кінець, якщо ви можете дозволити URL-адресу бути необов’язковою: клас OptionalUrlValidator <UrlValidator def validate_each (запис, атрибут, значення) повернути true, якщо value.blank? повернути супер кінець кінця
Брудний Генрі

1
Це не дуже хороша перевірка:URI("http:").kind_of?(URI::HTTP) #=> true
сміття

29

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

Просто додайте

gem 'validate_url'

до своїх Gemfile, а потім у моделях можна

validates :click_through_url, url: true

@ ЕвгенийМасленков, це може бути так само добре, оскільки він дійсний відповідно до специфікації, але ви можете перевірити github.com/sporkmonger/addressable/isissue . Також у загальному випадку ми виявили, що ніхто не дотримується стандарту, а натомість використовують просту перевірку формату.
dolzenko

13

На це питання вже відповіли, але що, чорт, пропоную рішення, яке я використовую.

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

І нарешті, ми намагаємося отримати сторінку. Можливо, я повинен приймати переадресації і не тільки HTTP 200 ОК.

# 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

дійсно акуратно! дякую за ваш внесок, часто існує багато підходів до проблеми; це здорово, коли люди діляться своїм.
Джей

6
Я просто хотів зазначити, що відповідно до посібника з безпеки рейлів, ви повинні використовувати \ A і \ z, а не $ ^ в цьому регекспі
Джаред

1
Мені це подобається. Швидка пропозиція трохи висушити код, перемістивши регулярний вираз у валідатор, як я думаю, ви хочете, щоб він був узгодженим для всіх моделей. Бонус: Це дозволить вам перенести перший рядок під validate_each.
Пол Петтенгілл

Що робити, якщо URL-адреса триває та очікує час? Що буде найкращим варіантом для відображення повідомлення про помилку в очікуванні часу або якщо сторінку не можна відкрити?
user588324

це ніколи не буде проходити аудит безпеки, ви змушуєте ваші сервери пробувати довільну URL-адресу
Маурісіо

12

Ви також можете спробувати valid_url gem, який дозволяє URL-адреси без схеми, перевіряє доменну зону та ip-імена хостів.

Додайте його до свого Gemfile:

gem 'valid_url'

І тоді в моделі:

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

Це дуже приємно, особливо URL-адреси без схеми, що дивно пов'язане з класом URI.
Пол Петтенгілл

Мене здивувала здатність цього дорогоцінного каміння копати IP-адреси на основі IP-адрес та виявляти фальшиві. Дякую!
Візз Оз

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-адреси параметра.


1
дякую за ваш внесок, завжди добре бачити різні рішення
січні

До речі, ваш regexp буде відхиляти дійсні 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/#". У Рубі 2.1.6 оцінка висить.
Старий Про

Ви маєте рацію, здається, що в деяких випадках цей регулярний вираз повинен назавжди вирішити :(
heriberto perez

1
Очевидно, що не існує регулярного виразу, який охоплює кожен сценарій, тому я закінчую простою валідацією: validates: url, format: {with: URI.regexp}, якщо: Proc.new {| a | a.url.present? }
heriberto perez

5

Нещодавно я зіткнувся з тією ж проблемою (мені потрібно було перевірити URL-адреси в додатку Rails), але мені довелося впоратися з додатковою вимогою унікодних URL-адрес (наприклад 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"або Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')абсолютно добре з точки зору
Addressable

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

Зауважте, що все ще існують дивні URI-адреси HTTP, які розбираються як дійсні адреси.

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

Ось питання для addressableдорогоцінного каміння, який висвітлює приклади.


3

Я використовую невелику варіацію розчину лафебера вище . Він забороняє послідовні точки в імені хоста (наприклад, у 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)?
Крейг


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-адреса існує, надсилаючи запит.


Що робити, якщо URL вказує на дуже великий ресурс (скажімо, кілька гігабайт)?
Джон Шнайдер

@JonSchneider можна використати головний запит http (наприклад, тут ) замість get.
wvengen

1

Мені сподобалося, щоб маніпуляція з 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-адресу. Просто включаючи варіанти.


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',\|\(\).:;?\-\[\]>\)])
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.