Rails: Jaki jest dobry sposób sprawdzania poprawności linków (adresów URL)?

125

Zastanawiałem się, jak najlepiej zweryfikować adresy URL w Railsach. Myślałem o użyciu wyrażenia regularnego, ale nie jestem pewien, czy to najlepsza praktyka.

A gdybym użył wyrażenia regularnego, czy ktoś mógłby mi go zasugerować? Nadal jestem nowy w Regex.

sójka
źródło
Związane z: stackoverflow.com/questions/1805761/…
Jon Schneider

Odpowiedzi:

151

Weryfikacja adresu URL to trudna praca. To także bardzo szeroka prośba.

Co dokładnie chcesz zrobić? Czy chcesz sprawdzić format adresu URL, istnienie czy co? Istnieje kilka możliwości, w zależności od tego, co chcesz zrobić.

Wyrażenie regularne może zweryfikować format adresu URL. Ale nawet złożone wyrażenie regularne nie może zapewnić, że masz do czynienia z prawidłowym adresem URL.

Na przykład, jeśli weźmiesz proste wyrażenie regularne, prawdopodobnie odrzuci ono następujący host

http://invalid##host.com

ale to pozwoli

http://invalid-host.foo

to jest prawidłowy host, ale nie jest to prawidłowa domena, jeśli wziąć pod uwagę istniejące TLD. Rzeczywiście, rozwiązanie zadziałałoby, gdybyś chciał zweryfikować nazwę hosta, a nie domenę, ponieważ następująca jest prawidłową nazwą hosta

http://host.foo

jak również następny

http://localhost

Pozwólcie, że przedstawię wam kilka rozwiązań.

Jeśli chcesz zweryfikować domenę, musisz zapomnieć o wyrażeniach regularnych. Najlepszym obecnie dostępnym rozwiązaniem jest Public Suffix List, lista utrzymywana przez Mozillę. Stworzyłem bibliotekę Ruby do analizowania i sprawdzania poprawności domen względem listy sufiksów publicznych i nazywa się PublicSuffix .

Jeśli chcesz sprawdzić poprawność formatu identyfikatora URI / adresu URL, możesz użyć wyrażeń regularnych. Zamiast szukać jednego, użyj wbudowanej URI.parsemetody Ruby .

require 'uri'

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

Możesz nawet zdecydować, czy będzie bardziej restrykcyjny. Na przykład, jeśli chcesz, aby adres URL był adresem URL HTTP / HTTPS, możesz zwiększyć dokładność weryfikacji.

require 'uri'

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

Oczywiście istnieje mnóstwo ulepszeń, które możesz zastosować w tej metodzie, w tym sprawdzenie ścieżki lub schematu.

Wreszcie, możesz również spakować ten kod do walidatora:

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
Simone Carletti
źródło
1
Zauważ, że klasa będzie przeznaczona URI::HTTPSdla https uris (np .:URI.parse("https://yo.com").class => URI::HTTPS
tee
12
URI::HTTPSdziedziczy po URI:HTTP, dlatego używam kind_of?.
Simone Carletti,
1
Zdecydowanie najbardziej kompletne rozwiązanie do bezpiecznego sprawdzania poprawności adresu URL.
Fabrizio Regini
4
URI.parse('http://invalid-host.foo')zwraca prawdę, ponieważ ten identyfikator URI jest prawidłowym adresem URL. Należy również pamiętać, że .foojest to teraz prawidłowa TLD. iana.org/domains/root/db/foo.html
Simone Carletti
1
@jmccartie, przeczytaj cały post. Jeśli zależy Ci na schemacie, powinieneś użyć końcowego kodu, który zawiera również sprawdzenie typu, a nie tylko ten wiersz. Przestałeś czytać przed końcem postu.
Simone Carletti
101

Używam jednej wkładki wewnątrz moich modeli:

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

Myślę, że jest wystarczająco dobry i prosty w użyciu. Ponadto powinno być teoretycznie równoważne metodzie Simone, ponieważ wewnętrznie używa tego samego wyrażenia regularnego.

Matteo Collina
źródło
17
Niestety 'http://'pasuje do powyższego wzorca. Zobacz:URI::regexp(%w(http https)) =~ 'http://'
David J.,
15
Również adres URL, taki jak, http:fakebędzie ważny.
nathanvda,
54

Zgodnie z pomysłem Simone możesz łatwo stworzyć własny walidator.

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

a następnie użyj

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

w swoim modelu.

jlfenaux
źródło
1
gdzie mam umieścić tę klasę? W inicjatorze?
debiut
3
Cytuję z @gbc: „Jeśli umieścisz własne walidatory w aplikacji / walidatorach, zostaną one automatycznie załadowane bez konieczności zmiany pliku config / application.rb”. ( stackoverflow.com/a/6610270/839847 ). Zauważ, że poniższa odpowiedź od Stefana Petterssona pokazuje, że zapisał on również podobny plik w „app / validators”.
bergie3000
4
sprawdza tylko, czy adres URL zaczyna się od http: // lub https: //, nie jest to poprawna weryfikacja adresu URL
maggix
1
Zakończ, jeśli możesz sobie pozwolić na to, aby URL był opcjonalny: class OptionalUrlValidator <UrlValidator def validate_each (rekord, atrybut, wartość) zwraca true, jeśli value.blank? powrót super koniec koniec
Dirty Henry
1
To nie jest dobra weryfikacja:URI("http:").kind_of?(URI::HTTP) #=> true
smathy
29

Istnieje również gem validate_url (który jest po prostu ładnym opakowaniem Addressable::URI.parserozwiązania).

Poprostu dodaj

gem 'validate_url'

do twojego Gemfile, a potem w modelach możesz

validates :click_through_url, url: true
dolzenko
źródło
@ ЕвгенийМасленков to może być równie dobre, ponieważ jest poprawne zgodnie ze specyfikacją, ale możesz sprawdzić github.com/sporkmonger/addressable/issues . Również w ogólnym przypadku stwierdziliśmy, że nikt nie przestrzega standardu i zamiast tego używa prostego sprawdzania poprawności formatu.
dolzenko
13

Na to pytanie mam już odpowiedź, ale co do cholery, proponuję rozwiązanie, którego używam.

Wyrażenie regularne działa dobrze ze wszystkimi adresami URL, które spotkałem. Metoda ustawiająca polega na zachowaniu ostrożności, jeśli żaden protokół nie jest wymieniony (załóżmy, że http: //).

Na koniec próbujemy pobrać stronę. Może powinienem akceptować przekierowania, a nie tylko 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

i...

# 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
Stefan Pettersson
źródło
naprawdę fajnie! dzięki za wkład, często istnieje wiele podejść do problemu; wspaniale jest, gdy ludzie udostępniają swoje.
sójka
6
Chciałem tylko zaznaczyć, że zgodnie z przewodnikiem po zabezpieczeniach szyn powinieneś użyć \ A i \ z zamiast $ ^ w tym wyrażeniu regularnym
Jared
1
Lubię to. Szybka sugestia, aby nieco wysuszyć kod, przenosząc wyrażenie regularne do walidatora, ponieważ wyobrażam sobie, że chciałbyś, aby był spójny we wszystkich modelach. Bonus: Pozwoliłoby ci upuścić pierwszą linię pod validate_each.
Paul Pettengill,
Co się stanie, jeśli adres URL trwa długo i przekracza limit czasu? Jaka będzie najlepsza opcja, aby wyświetlić komunikat o błędzie przekroczenia limitu czasu lub jeśli nie można otworzyć strony?
user588324
to nigdy nie przejdzie audytu bezpieczeństwa, sprawiasz, że twoje serwery wbijają dowolny adres URL
Mauricio
12

Możesz także wypróbować gem valid_url, który zezwala na adresy URL bez schematu, sprawdza strefę domeny i nazwy hostów IP.

Dodaj go do swojego Gemfile:

gem 'valid_url'

A potem w modelu:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Roman Ralovets
źródło
To takie miłe, zwłaszcza adresy URL bez schematu, co jest zaskakująco związane z klasą URI.
Paul Pettengill,
Zaskoczyła mnie zdolność tego klejnotu do przeszukiwania adresów URL opartych na adresach IP i wykrywania fałszywych. Dzięki!
The Whiz of Oz
10

Tylko moje 2 centy:

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

EDYCJA: zmieniono wyrażenie regularne, aby pasowało do adresów URL parametrów.

lafeber
źródło
1
dzięki za wkład, zawsze dobrze jest zobaczyć różne rozwiązania
jay
Przy okazji, twoje http://test.com/fdsfsdf?a=b
wyrażenie regularne
2
Wprowadziliśmy ten kod do produkcji i ciągle otrzymywaliśmy limity czasu w nieskończonych pętlach w wierszu wyrażenia regularnego .match. Nie wiem dlaczego, po prostu uważaj na niektóre przypadki narożne i chciałbym usłyszeć opinie innych o tym, dlaczego tak się dzieje.
toobulkeh
10

Rozwiązanie, które działało dla mnie, to:

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

Próbowałem użyć jakiegoś przykładu, który dołączyłeś, ale obsługuję adres URL w następujący sposób:

Zwróć uwagę na użycie A i Z, ponieważ jeśli użyjesz ^ i $, zobaczysz ostrzeżenie od walidatorów 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'
heriberto perez
źródło
1
Spróbuj tego z "https://portal.example.com/portal/#". W Rubim 2.1.6 ocena się zawiesza.
Old Pro
masz rację, wydaje się, że w niektórych przypadkach rozwiązanie tego wyrażenia regularnego trwa wiecznie :(
heriberto perez
1
oczywiście nie ma wyrażenia regularnego, które obejmowałoby każdy scenariusz, dlatego używam tylko prostej walidacji: validates: url, format: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
heriberto perez
5

Ostatnio napotkałem ten sam problem (musiałem zweryfikować adresy URL w aplikacji Rails), ale musiałem poradzić sobie z dodatkowym wymogiem adresów URL Unicode (np. http://кц.рф ) ...

Zbadałem kilka rozwiązań i znalazłem następujące:

  • Pierwszą i najbardziej sugerowaną rzeczą jest użycie URI.parse . Sprawdź odpowiedź Simone Carletti, aby poznać szczegóły. Działa to dobrze, ale nie w przypadku adresów URL Unicode.
  • Drugą metodą, którą zobaczyłem, była metoda Ilyi Grigorik: http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/ Zasadniczo, stara się złożyć wniosek do url; jeśli działa, to jest ważne ...
  • Trzecia metoda, którą znalazłem (i ta, którą preferuję) jest podejściem podobnym, URI.parseale używającym addressablegem zamiast standardowej URIbiblioteki. To podejście jest szczegółowo opisane tutaj: http://rawsyntax.com/blog/url-validation-in-rails-3-and-ruby-in-general/
severin
źródło
Tak, ale Addressable::URI.parse('http:///').scheme # => "http"lub Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')są w porządku z punktu widzenia
Adresable
4

Oto zaktualizowana wersja walidatora opublikowana przez Davida Jamesa . Został opublikowany przez Benjamina Fleischera . W międzyczasie wcisnąłem zaktualizowany widelec, który można znaleźć tutaj .

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

Proszę zauważyć, że wciąż istnieją dziwne identyfikatory URI HTTP, które są analizowane jako prawidłowe adresy.

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

Oto kwestia addressableklejnotu, która obejmuje przykłady.

JJD
źródło
3

Używam niewielkiej odmiany powyższego rozwiązania Lafeber . Zabrania kolejnych kropek w nazwie hosta (na przykład w www.many...dots.com):

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

URI.parsewydaje się narzucać prefiksowanie schematu, co w niektórych przypadkach nie jest tym, czego możesz chcieć (np. jeśli chcesz umożliwić użytkownikom szybkie pisanie adresów URL w formularzach takich jak twitter.com/username)

Franco
źródło
2

I zostały z wykorzystaniem gem „activevalidators” i że to działa całkiem dobrze (nie tylko dla URL walidacji)

można go znaleźć tutaj

Wszystko jest udokumentowane, ale w zasadzie po dodaniu klejnotu będziesz chciał dodać następujące kilka wierszy w inicjatorze, powiedz: /config/environments/initializers/active_validators_activation.rb

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

(Uwaga: możesz zamienić: all na: url lub: cokolwiek, jeśli chcesz tylko zweryfikować określone typy wartości)

A potem w swoim modelu coś takiego

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

Teraz uruchom ponownie serwer i to powinno być to

Arnaud Bouchot
źródło
2

Jeśli potrzebujesz prostej weryfikacji i niestandardowego komunikatu o błędzie:

  validates :some_field_expecting_url_value,
            format: {
              with: URI.regexp(%w[http https]),
              message: 'is not a valid URL'
            }
Caleb
źródło
1

Możesz zweryfikować wiele adresów URL za pomocą czegoś takiego:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Damien Roche
źródło
1
Jak poradzisz sobie z adresami URL bez schematu (np. Www.bar.com/foo)?
craig
1

Ostatnio miałem ten sam problem i znalazłem obejście dla prawidłowych adresów 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

Pierwsza część metody validate_url wystarczy do sprawdzenia poprawności formatu adresu URL. Druga część upewni się, że adres URL istnieje, wysyłając żądanie.

Dilnavaz
źródło
Co się stanie, jeśli adres URL wskazuje na bardzo duży zasób (na przykład wiele gigabajtów)?
Jon Schneider
@JonSchneider zamiast get można użyć żądania nagłówka http (jak tutaj ).
wvengen
1

Podobało mi się monkeypatch moduł URI, aby dodać prawidłowe? metoda

wewnątrz 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
Blair Anderson
źródło
0

I jako moduł

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

A potem include UrlValidatorw dowolnym modelu, dla którego chcesz sprawdzić poprawność adresu URL. Tylko w tym dla opcji.

MCB
źródło
0

Walidacji adresu URL nie można przeprowadzić po prostu za pomocą wyrażenia regularnego, ponieważ liczba witryn internetowych stale rośnie i pojawiają się nowe schematy nazewnictwa domen.

W moim przypadku po prostu piszę niestandardowy walidator, który sprawdza pomyślną odpowiedź.

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

Sprawdzam pathatrybut mojego modelu przy użyciu record.path. Przekazuję również błąd do odpowiedniej nazwy atrybutu przy użyciurecord.errors[:path] .

Możesz po prostu zastąpić to dowolną nazwą atrybutu.

Następnie po prostu dzwonię do niestandardowego walidatora w moim modelu.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Noman Ur Rehman
źródło
Co się stanie, jeśli adres URL wskazuje na bardzo duży zasób (na przykład wiele gigabajtów)?
Jon Schneider,
0

Możesz do tego użyć wyrażenia regularnego, dla mnie działa dobrze ten:

(^|[\s.:;?\-\]<\(])(ftp|https?:\/\/[-\w;\/?:@&=+$\|\_.!~*\|'()\[\]%#,]+[\w\/#](\(\))?)(?=$|[\s',\|\(\).:;?\-\[\]>\)])
spirito_libero
źródło