Rails: Was ist ein guter Weg, um Links (URLs) zu validieren?

125

Ich habe mich gefragt, wie ich URLs in Rails am besten validieren kann. Ich habe überlegt, einen regulären Ausdruck zu verwenden, bin mir aber nicht sicher, ob dies die beste Vorgehensweise ist.

Und wenn ich einen regulären Ausdruck verwenden würde, könnte mir jemand einen vorschlagen? Ich bin noch neu bei Regex.

Jay
quelle
Siehe auch
Jon Schneider

Antworten:

151

Das Überprüfen einer URL ist eine schwierige Aufgabe. Es ist auch eine sehr breite Anfrage.

Was genau möchten Sie tun? Möchten Sie das Format der URL, die Existenz oder was überprüfen? Es gibt verschiedene Möglichkeiten, je nachdem, was Sie tun möchten.

Ein regulärer Ausdruck kann das Format der URL überprüfen. Aber selbst ein komplexer regulärer Ausdruck kann nicht sicherstellen, dass es sich um eine gültige URL handelt.

Wenn Sie beispielsweise einen einfachen regulären Ausdruck verwenden, wird der folgende Host wahrscheinlich abgelehnt

http://invalid##host.com

aber es wird erlauben

http://invalid-host.foo

Dies ist ein gültiger Host, aber keine gültige Domäne, wenn Sie die vorhandenen TLDs berücksichtigen. In der Tat würde die Lösung funktionieren, wenn Sie den Hostnamen und nicht die Domäne überprüfen möchten, da der folgende ein gültiger Hostname ist

http://host.foo

sowie die folgende

http://localhost

Lassen Sie mich Ihnen nun einige Lösungen geben.

Wenn Sie eine Domain validieren möchten, müssen Sie reguläre Ausdrücke vergessen. Die derzeit beste verfügbare Lösung ist die Public Suffix List, eine Liste, die von Mozilla verwaltet wird. Ich habe eine Ruby-Bibliothek erstellt, um Domänen anhand der öffentlichen Suffix-Liste zu analysieren und zu validieren. Sie heißt PublicSuffix .

Wenn Sie das Format eines URI / einer URL überprüfen möchten, möchten Sie möglicherweise reguläre Ausdrücke verwenden. Verwenden Sie die integrierte Ruby- URI.parseMethode, anstatt nach einer zu suchen .

require 'uri'

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

Sie können sogar entscheiden, es restriktiver zu gestalten. Wenn Sie beispielsweise möchten, dass die URL eine HTTP / HTTPS-URL ist, können Sie die Validierung genauer gestalten.

require 'uri'

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

Natürlich gibt es unzählige Verbesserungen, die Sie auf diese Methode anwenden können, einschließlich der Suche nach einem Pfad oder einem Schema.

Zu guter Letzt können Sie diesen Code auch in einen Validator packen:

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
quelle
1
Beachten Sie, dass die Klasse URI::HTTPSfür https uris sein wird (Beispiel:URI.parse("https://yo.com").class => URI::HTTPS
Tee
12
URI::HTTPSerbt von URI:HTTP, das ist der Grund, warum ich benutze kind_of?.
Simone Carletti
1
Mit Abstand die vollständigste Lösung zur sicheren Validierung einer URL.
Fabrizio Regini
4
URI.parse('http://invalid-host.foo')Gibt true zurück, da dieser URI eine gültige URL ist. Beachten Sie auch, dass dies .foojetzt eine gültige TLD ist. iana.org/domains/root/db/foo.html
Simone Carletti
1
@jmccartie bitte lesen Sie den gesamten Beitrag. Wenn Sie sich für das Schema interessieren, sollten Sie den endgültigen Code verwenden, der auch eine Typprüfung enthält, nicht nur diese Zeile. Sie haben vor dem Ende des Beitrags aufgehört zu lesen.
Simone Carletti
101

Ich benutze einen Einzeiler in meinen Modellen:

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

Ich denke, es ist gut genug und einfach zu bedienen. Darüber hinaus sollte es theoretisch der Simone-Methode entsprechen, da es intern denselben regulären Ausdruck verwendet.

Matteo Collina
quelle
17
Entspricht leider 'http://'dem obigen Muster. Siehe:URI::regexp(%w(http https)) =~ 'http://'
David J.
15
Auch eine URL wie http:fakeist gültig.
Nathanvda
54

Nach Simones Idee können Sie ganz einfach Ihren eigenen Validator erstellen.

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

und dann verwenden

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

in Ihrem Modell.

jlfenaux
quelle
1
Wo soll ich diese Klasse platzieren? In einem Initialisierer?
Deb
3
Ich zitiere aus @gbc: "Wenn Sie Ihre benutzerdefinierten Validatoren in app / validators platzieren, werden sie automatisch geladen, ohne dass Sie Ihre config / application.rb-Datei ändern müssen." ( stackoverflow.com/a/6610270/839847 ). Beachten Sie, dass die Antwort von Stefan Pettersson unten zeigt, dass er eine ähnliche Datei auch in "app / validators" gespeichert hat.
bergie3000
4
Dies prüft nur, ob die URL mit http: // oder https: // beginnt. Es handelt sich nicht um eine ordnungsgemäße URL-Validierung
Maggix
1
Beenden Sie, wenn Sie sich leisten können, dass die URL optional ist: class OptionalUrlValidator <UrlValidator def validate_each (Datensatz, Attribut, Wert) return true, wenn value.blank? Rückkehr Super End End
Dirty Henry
1
Dies ist keine gute URI("http:").kind_of?(URI::HTTP) #=> true
Bestätigung
28

Es gibt auch validate_url gem (was nur ein netter Wrapper für istAddressable::URI.parse Lösung ist).

Einfach hinzufügen

gem 'validate_url'

zu Ihrem Gemfile, und dann in Modellen können Sie

validates :click_through_url, url: true
Dolzenko
quelle
@ ЕвгенийМасленков das könnte genauso gut sein, weil es gemäß der Spezifikation gültig ist, aber Sie möchten vielleicht github.com/sporkmonger/addressable/issues überprüfen . Auch im Allgemeinen haben wir festgestellt, dass niemand dem Standard folgt und stattdessen eine einfache Formatvalidierung verwendet.
Dolzenko
13

Diese Frage ist bereits beantwortet, aber was solls, ich schlage die Lösung vor, die ich verwende.

Der reguläre Ausdruck funktioniert gut mit allen URLs, die ich getroffen habe. Die Setter-Methode besteht darin, darauf zu achten, dass kein Protokoll erwähnt wird (nehmen wir an, http: //).

Und schließlich versuchen wir, die Seite abzurufen. Vielleicht sollte ich Weiterleitungen akzeptieren und nicht nur 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

und...

# 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
quelle
wirklich ordentlich! Vielen Dank für Ihre Eingabe, es gibt oft viele Ansätze für ein Problem; Es ist großartig, wenn Leute ihre teilen.
Jay
6
Ich wollte nur darauf hinweisen, dass Sie gemäß dem Rails-Sicherheitsleitfaden \ A und \ z anstelle von $ ^ in diesem regulären Ausdruck verwenden sollten
Jared
1
Ich mag das. Schneller Vorschlag, den Code ein wenig auszutrocknen, indem Sie den regulären Ausdruck in den Validator verschieben, da ich mir vorstellen kann, dass er modellübergreifend konsistent sein soll. Bonus: Damit können Sie die erste Zeile unter validate_each ablegen.
Paul Pettengill
Was ist, wenn die URL lange dauert und eine Zeitüberschreitung auftritt? Was ist die beste Option, um die Timeout-Fehlermeldung anzuzeigen oder wenn die Seite nicht geöffnet werden kann?
user588324
Dies würde niemals eine Sicherheitsüberprüfung bestehen. Sie lassen Ihre Server eine beliebige URL eingeben
Mauricio
12

Sie können auch valid_url ausprobieren gem das URLs ohne das Schema zulässt und die IP-Hostnamen überprüft.

Fügen Sie es Ihrem Gemfile hinzu:

gem 'valid_url'

Und dann im Modell:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Roman Ralovets
quelle
Das ist so schön, besonders die URLs ohne Schema, die überraschenderweise mit der URI-Klasse zu tun haben.
Paul Pettengill
Ich war überrascht von der Fähigkeit dieses Edelsteins, IP-basierte URLs zu durchsuchen und die falschen zu erkennen. Vielen Dank!
The Whiz of Oz
10

Nur meine 2 Cent:

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

BEARBEITEN: Regex wurde geändert, um mit den Parameter-URLs übereinzustimmen.

lafeber
quelle
1
Vielen Dank für Ihre Eingabe, immer gut, verschiedene Lösungen zu sehen
Jay
Übrigens wird Ihr regulärer Ausdruck gültige URLs mit Abfragezeichenfolge wiehttp://test.com/fdsfsdf?a=b
MikDiet
2
Wir haben diesen Code in Produktion genommen und immer wieder Timeouts für Endlosschleifen in der .match-Regex-Zeile erhalten. Ich bin mir nicht sicher warum, sei nur vorsichtig bei einigen Eckfällen und würde gerne die Gedanken anderer hören, warum dies passieren würde.
Toobulkeh
10

Die Lösung, die für mich funktioniert hat, war:

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

Ich habe versucht, einige der von Ihnen angehängten Beispiele zu verwenden, aber ich unterstütze die URL wie folgt:

Beachten Sie die Verwendung von A und Z, da bei Verwendung von ^ und $ diese Warnsicherheit von Rails-Validatoren angezeigt wird.

 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
quelle
1
Versuchen Sie dies mit "https://portal.example.com/portal/#". In Ruby 2.1.6 hängt die Auswertung.
Old Pro
Sie haben Recht, in einigen Fällen scheint es ewig zu dauern, bis dieser reguläre Ausdruck gelöst ist :(
heriberto perez
1
Offensichtlich gibt es keinen regulären Ausdruck, der jedes Szenario abdeckt. Deshalb verwende ich am Ende nur eine einfache Validierung: validates: url, format: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
Heriberto Perez
5

Ich bin in letzter Zeit auf dasselbe Problem gestoßen (ich musste URLs in einer Rails-App validieren), musste aber die zusätzlichen Anforderungen an Unicode-URLs (z http://кц.рф ) ...

Ich habe nach einigen Lösungen gesucht und bin auf Folgendes gestoßen:

  • Das erste und am meisten vorgeschlagene ist die Verwendung URI.parse . Überprüfen Sie die Antwort von Simone Carletti für Details. Dies funktioniert in Ordnung, aber nicht für Unicode-URLs.
  • Die zweite Methode, die ich gesehen habe, war die von Ilya Grigorik: http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/ Grundsätzlich versucht er, eine Anfrage an die zu URL; wenn es funktioniert, ist es gültig ...
  • Die dritte Methode, die ich gefunden habe (und die, die ich bevorzuge), ist ein ähnlicher Ansatz, bei dem URI.parsejedoch der addressableEdelstein anstelle der URIstdlib verwendet wird. Dieser Ansatz wird hier detailliert beschrieben: http://rawsyntax.com/blog/url-validation-in-rails-3-and-ruby-in-general/
Severin
quelle
Ja, aber Addressable::URI.parse('http:///').scheme # => "http"oder Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')sind aus der Sicht von Addressable vollkommen in
Ordnung
4

Hier ist eine aktualisierte Version des Validators von David James . Es wurde von Benjamin Fleischer veröffentlicht . In der Zwischenzeit habe ich eine aktualisierte Gabel geschoben, die hier zu finden ist .

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

Bitte beachten Sie, dass es immer noch seltsame HTTP-URIs gibt, die als gültige Adressen analysiert werden.

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

Hier ist eine Ausgabe für den addressableEdelstein, die die Beispiele abdeckt.

JJD
quelle
3

Ich verwende oben eine leichte Variation der Lafeber-Lösung . Aufeinanderfolgende Punkte im Hostnamen sind nicht zulässig (z. B. in www.many...dots.com):

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

URI.parsescheint Schema Vorfixierung zu beauftragen, die in einigen Fällen ist nicht das, was Sie wollen (zB wenn Sie möchten , dass Ihre Benutzer ermöglichen, schnell URLs in Formen wie buchstabieren können twitter.com/username)

Franco
quelle
2

Ich habe das Juwel 'activevalidators' verwendet und es funktioniert ziemlich gut (nicht nur für die Validierung von URLs).

Sie finden es hier

Es ist alles dokumentiert, aber im Grunde genommen möchten Sie nach dem Hinzufügen des Edelsteins die folgenden Zeilen in einen Initialisierer einfügen: /config/environments/initializers/active_validators_activation.rb

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

(Hinweis: Sie können Folgendes ersetzen: alle durch: url oder: was auch immer, wenn Sie nur bestimmte Wertetypen validieren möchten.)

Und dann zurück in Ihrem Modell so etwas

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

Jetzt Starten Sie den Server und das sollte es sein ,

Arnaud Bouchot
quelle
2

Wenn Sie eine einfache Validierung und eine benutzerdefinierte Fehlermeldung wünschen:

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

Sie können mehrere URLs mit folgenden Methoden überprüfen:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Damien Roche
quelle
1
Wie würden Sie mit URLs ohne das Schema umgehen (z. B. www.bar.com/foo)?
Craig
1

Vor kurzem hatte ich das gleiche Problem und fand eine Lösung für gültige URLs.

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

Der erste Teil der validate_url-Methode reicht aus, um das URL-Format zu validieren. Im zweiten Teil wird durch Senden einer Anfrage sichergestellt, dass die URL vorhanden ist.

Dilnavaz
quelle
Was ist, wenn die URL auf eine Ressource verweist, die sehr groß ist (z. B. mehrere Gigabyte)?
Jon Schneider
@ JonSchneider könnte man eine http head Anfrage (wie hier ) verwenden anstatt zu bekommen.
wvengen
1

Ich wollte das URI-Modul monkeypatch, um das gültige hinzuzufügen? Methode

Innerhalb 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
quelle
0

Und als Modul

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

Und dann einfach include UrlValidatorin jedem Modell, für das Sie URLs validieren möchten. Nur für Optionen.

MCB
quelle
0

Die URL-Validierung kann nicht einfach mithilfe eines regulären Ausdrucks durchgeführt werden, da die Anzahl der Websites weiter zunimmt und immer neue Domain-Namensschemata auftauchen.

In meinem Fall schreibe ich einfach einen benutzerdefinierten Validator, der nach einer erfolgreichen Antwort sucht.

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

Ich überprüfe das pathAttribut meines Modells mithilfe von record.path. Ich schiebe den Fehler auch mit auf den jeweiligen Attributnamenrecord.errors[:path] .

Sie können dies einfach durch einen beliebigen Attributnamen ersetzen.

Dann rufe ich einfach den benutzerdefinierten Validator in meinem Modell auf.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Noman Ur Rehman
quelle
Was ist, wenn die URL auf eine Ressource verweist, die sehr groß ist (z. B. mehrere Gigabyte)?
Jon Schneider
0

Sie könnten Regex dafür verwenden, für mich funktioniert dieses gut:

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