Rails: qual è un buon modo per convalidare i link (URL)?


Risposte:


151

La convalida di un URL è un lavoro complicato. È anche una richiesta molto ampia.

Cosa vuoi fare esattamente? Vuoi convalidare il formato dell'URL, l'esistenza o cosa? Ci sono diverse possibilità, a seconda di cosa vuoi fare.

Un'espressione regolare può convalidare il formato dell'URL. Ma anche un'espressione regolare complessa non può garantire che si tratti di un URL valido.

Ad esempio, se prendi una semplice espressione regolare, probabilmente rifiuterà il seguente host

http://invalid##host.com

ma permetterà

http://invalid-host.foo

questo è un host valido, ma non un dominio valido se si considerano i TLD esistenti. In effetti, la soluzione funzionerebbe se si desidera convalidare il nome host, non il dominio perché il seguente è un nome host valido

http://host.foo

così come il seguente

http://localhost

Ora, lascia che ti dia alcune soluzioni.

Se vuoi convalidare un dominio, devi dimenticare le espressioni regolari. La migliore soluzione disponibile al momento è la Public Suffix List, una lista mantenuta da Mozilla. Ho creato una libreria Ruby per analizzare e convalidare i domini rispetto all'elenco dei suffissi pubblici e si chiama PublicSuffix .

Se desideri convalidare il formato di un URI / URL, potresti voler utilizzare espressioni regolari. Invece di cercarne uno, usa il URI.parsemetodo Ruby integrato .

require 'uri'

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

Puoi anche decidere di renderlo più restrittivo. Ad esempio, se desideri che l'URL sia un URL HTTP / HTTPS, puoi rendere la convalida più accurata.

require 'uri'

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

Naturalmente, ci sono tonnellate di miglioramenti che puoi applicare a questo metodo, incluso il controllo di un percorso o di uno schema.

Ultimo ma non meno importante, puoi anche impacchettare questo codice in un validatore:

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
Nota che la lezione sarà URI::HTTPSper https uris (es:URI.parse("https://yo.com").class => URI::HTTPS
tee

12
URI::HTTPSeredita da URI:HTTP, questo è il motivo per cui uso kind_of?.
Simone Carletti

1
Di gran lunga la soluzione più completa per convalidare in sicurezza un URL.
Fabrizio Regini

4
URI.parse('http://invalid-host.foo')restituisce true perché quell'URI è un URL valido. Si noti inoltre che .fooora è un TLD valido. iana.org/domains/root/db/foo.html
Simone Carletti

1
@jmccartie per favore leggi l'intero post. Se ti interessa lo schema, dovresti usare il codice finale che include anche un controllo del tipo, non solo quella riga. Hai smesso di leggere prima della fine del post.
Simone Carletti

101

Uso una linea all'interno dei miei modelli:

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

Penso che sia abbastanza buono e semplice da usare. Inoltre dovrebbe essere teoricamente equivalente al metodo di Simone, in quanto utilizza internamente la stessa regexp.


17
Purtroppo 'http://'corrisponde al modello sopra. Vedi:URI::regexp(%w(http https)) =~ 'http://'
David J.

15
Anche un URL simile http:fakesarà valido.
nathanvda

54

Seguendo l'idea di Simone, puoi facilmente creare il tuo validatore.

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

e poi usa

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

nel tuo modello.


1
dove devo mettere questa classe? In un inizializzatore?
deb

3
Cito da @gbc: "Se metti i tuoi validatori personalizzati in app / validatori, verranno caricati automaticamente senza bisogno di alterare il tuo file config / application.rb." ( stackoverflow.com/a/6610270/839847 ). Nota che la risposta di seguito da Stefan Pettersson mostra che ha salvato anche un file simile in "app / validatori".
bergie3000

4
questo controlla solo se l'URL inizia con http: // o https: //, non è una corretta convalida dell'URL
maggix

1
Termina se puoi permetterti che l'URL sia facoltativo: class OptionalUrlValidator <UrlValidator def validate_each (record, attributo, valore) return true if value.blank? ritorno super fine fine
Dirty Henry

1
Questa non è una buona convalida:URI("http:").kind_of?(URI::HTTP) #=> true
smathy

29

C'è anche validate_url gem (che è solo un bel wrapper per la Addressable::URI.parsesoluzione).

Basta aggiungere

gem 'validate_url'

al tuo Gemfile, e poi nei modelli puoi

validates :click_through_url, url: true

@ ЕвгенийМасленков potrebbe andare altrettanto bene perché è valido secondo le specifiche, ma potresti voler controllare github.com/sporkmonger/addressable/issues . Inoltre, in generale, abbiamo riscontrato che nessuno segue lo standard e utilizza invece una semplice convalida del formato.
dolzenko

13

Questa domanda ha già una risposta, ma che diamine, propongo la soluzione che sto usando.

La regexp funziona bene con tutti gli URL che ho incontrato. Il metodo setter è di fare attenzione se non viene menzionato alcun protocollo (supponiamo http: //).

Infine, proviamo a recuperare la pagina. Forse dovrei accettare reindirizzamenti e non solo 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

e...

# 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

davvero pulito! grazie per il tuo contributo, spesso ci sono molti approcci a un problema; è fantastico quando le persone condividono i propri.
jay

6
Volevo solo sottolineare che secondo la guida alla sicurezza di rails dovresti usare \ A e \ z invece di $ ^ in quella regexp
Jared

1
Mi piace. Suggerimento rapido per asciugare un po 'il codice spostando la regex nel validatore, poiché immagino che vorresti che fosse coerente tra i modelli. Bonus: ti consentirebbe di rilasciare la prima riga sotto validate_each.
Paul Pettengill

Cosa succede se l'URL sta impiegando molto tempo e timeout? Quale sarà l'opzione migliore per mostrare il messaggio di errore di timeout o se la pagina non può essere aperta?
user588324

questo non passerebbe mai un controllo di sicurezza, stai facendo in modo che i tuoi server puntino un URL arbitrario
Mauricio

12

Puoi anche provare valid_url gem che consente URL senza lo schema, controlla la zona del dominio e i nomi host ip.

Aggiungilo al tuo Gemfile:

gem 'valid_url'

E poi nel modello:

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

Questo è così bello, specialmente gli URL senza schema, che è sorprendentemente coinvolto con la classe URI.
Paul Pettengill

Sono rimasto sorpreso dalla capacità di questa gemma di scavare tra gli URL basati su IP e rilevare quelli fasulli. Grazie!
The Whiz of Oz

10

Solo i miei 2 centesimi:

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: regex modificato per abbinare gli URL dei parametri.


1
grazie per il tuo contributo, sempre bello vedere diverse soluzioni
jay

A proposito, la tua http://test.com/fdsfsdf?a=b
espressione regolare

2
Abbiamo messo questo codice in produzione e abbiamo continuato a ottenere timeout su loop infiniti sulla riga regex .match. Non sono sicuro del perché, solo cautela per alcune lettere maiuscole e mi piacerebbe sentire i pensieri degli altri sul motivo per cui ciò dovrebbe accadere.
toobulkeh

10

La soluzione che ha funzionato per me è stata:

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

Ho provato a utilizzare alcuni degli esempi che hai allegato ma sto supportando l'URL in questo modo:

Nota l'uso di A e Z perché se usi ^ e $ vedrai questo avviso di sicurezza dai validatori 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
Prova questo con "https://portal.example.com/portal/#". In Ruby 2.1.6 la valutazione si blocca.
Old Pro

hai ragione sembra che in alcuni casi questa espressione regolare richieda un'eternità per risolversi :(
heriberto perez

1
ovviamente, non esiste una regex che copre ogni scenario, ecco perché sto finendo per usare solo una semplice convalida: validates: url, format: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
heriberto perez

5

Ultimamente ho riscontrato lo stesso problema (avevo bisogno di convalidare gli URL in un'app Rails) ma ho dovuto far fronte al requisito aggiuntivo degli URL Unicode (ad es. http://кц.рф ) ...

Ho cercato un paio di soluzioni e ho riscontrato quanto segue:


Sì, ma Addressable::URI.parse('http:///').scheme # => "http"o Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')sono perfettamente ok dal punto di vista di Addressable :(
smileart

4

Ecco una versione aggiornata del validatore pubblicata da David James . È stato pubblicato da Benjamin Fleischer . Nel frattempo, ho spinto un fork aggiornato che può essere trovato qui .

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

Si noti che ci sono ancora strani URI HTTP che vengono analizzati come indirizzi validi.

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

Ecco un problema per la addressablegemma che copre gli esempi.


3

Uso una leggera variazione sulla soluzione di lafeber sopra . Non consente punti consecutivi nel nome host (come ad esempio in www.many...dots.com):

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

URI.parsesembra imporre il prefisso dello schema, che in alcuni casi non è quello che potresti desiderare (ad esempio se vuoi consentire ai tuoi utenti di scrivere rapidamente URL in forme come twitter.com/username)


2

Ho usato la gemma 'activevalidators' e funziona abbastanza bene (non solo per la convalida degli URL)

puoi trovarlo qui

È tutto documentato ma fondamentalmente una volta aggiunta la gemma, ti consigliamo di aggiungere le seguenti poche righe in un inizializzatore, ad esempio: /config/environments/initializers/active_validators_activation.rb

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

(Nota: puoi sostituire: all con: url o: qualunque cosa se vuoi solo convalidare tipi specifici di valori)

E poi di nuovo nel tuo modello qualcosa di simile

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

Ora riavvia il server e dovrebbe essere così


2

Se desideri una semplice convalida e un messaggio di errore personalizzato:

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

1

Puoi convalidare più URL usando qualcosa come:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true

1
Come gestireste gli URL senza lo schema (ad esempio www.bar.com/foo)?
Craig


1

Recentemente ho avuto lo stesso problema e ho trovato una soluzione per gli URL validi.

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

La prima parte del metodo validate_url è sufficiente per convalidare il formato dell'URL. La seconda parte si assicurerà che l'URL esista inviando una richiesta.


E se l'URL punta a una risorsa molto grande (ad esempio, più gigabyte)?
Jon Schneider,

@ JonSchneider si potrebbe usare una richiesta head http (come qui ) invece di get.
wvengen

1

Mi è piaciuto eseguire il monkeypatch del modulo URI per aggiungere il valido? metodo

dentro 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

E come modulo

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

E poi solo include UrlValidatorin qualsiasi modello per il quale desideri convalidare gli URL. Solo incluso per le opzioni.


0

La convalida dell'URL non può essere gestita semplicemente utilizzando un'espressione regolare poiché il numero di siti Web continua a crescere e continuano a emergere nuovi schemi di denominazione dei domini.

Nel mio caso, scrivo semplicemente un validatore personalizzato che controlla una risposta positiva.

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

Sto convalidando l' pathattributo del mio modello utilizzando record.path. Sto anche spingendo l'errore al rispettivo nome di attributo utilizzando record.errors[:path].

Puoi semplicemente sostituirlo con qualsiasi nome di attributo.

Quindi, chiamo semplicemente il validatore personalizzato nel mio modello.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end

E se l'URL punta a una risorsa molto grande (ad esempio, più gigabyte)?
Jon Schneider

0

Potresti usare regex per questo, per me funziona bene questo:

(^|[\s.:;?\-\]<\(])(ftp|https?:\/\/[-\w;\/?:@&=+$\|\_.!~*\|'()\[\]%#,]+[\w\/#](\(\))?)(?=$|[\s',\|\(\).:;?\-\[\]>\)])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.