Rails: ¿Cuál es una buena forma de validar enlaces (URL)?


Respuestas:


151

Validar una URL es un trabajo complicado. También es una solicitud muy amplia.

¿Qué quieres hacer exactamente? ¿Quieres validar el formato de la URL, la existencia o qué? Hay varias posibilidades, dependiendo de lo que quieras hacer.

Una expresión regular puede validar el formato de la URL. Pero incluso una expresión regular compleja no puede garantizar que esté tratando con una URL válida.

Por ejemplo, si toma una expresión regular simple, probablemente rechazará el siguiente host

http://invalid##host.com

pero permitirá

http://invalid-host.foo

ese es un host válido, pero no un dominio válido si considera los TLD existentes. De hecho, la solución funcionaría si desea validar el nombre de host, no el dominio porque el siguiente es un nombre de host válido

http://host.foo

así el siguiente

http://localhost

Ahora, déjame darte algunas soluciones.

Si desea validar un dominio, debe olvidarse de las expresiones regulares. La mejor solución disponible en este momento es la Lista de sufijos públicos, una lista mantenida por Mozilla. Creé una biblioteca Ruby para analizar y validar dominios en la lista de sufijos públicos, y se llama PublicSuffix .

Si desea validar el formato de un URI / URL, puede utilizar expresiones regulares. En lugar de buscar uno, use el URI.parsemétodo Ruby incorporado .

require 'uri'

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

Incluso puedes decidir hacerlo más restrictivo. Por ejemplo, si desea que la URL sea una URL HTTP / HTTPS, puede hacer que la validación sea más precisa.

require 'uri'

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

Por supuesto, hay toneladas de mejoras que puede aplicar a este método, incluida la comprobación de una ruta o un esquema.

Por último, pero no menos importante, también puede empaquetar este código en un validador:

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
Tenga en cuenta que la clase será URI::HTTPSpara https uris (ej:URI.parse("https://yo.com").class => URI::HTTPS
tee

12
URI::HTTPShereda de URI:HTTP, esa es la razón por la que uso kind_of?.
Simone Carletti

1
Con mucho, la solución más completa para validar con seguridad una URL.
Fabrizio Regini

44
URI.parse('http://invalid-host.foo')devuelve verdadero porque ese URI es una URL válida. También tenga en cuenta que .fooahora es un TLD válido. iana.org/domains/root/db/foo.html
Simone Carletti

1
@jmccartie por favor lea la publicación completa. Si le importa el esquema, debe usar el código final que incluye también una verificación de tipo, no solo esa línea. Dejaste de leer antes del final de la publicación.
Simone Carletti

101

Yo uso un forro dentro de mis modelos:

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

Creo que es lo suficientemente bueno y fácil de usar. Además, debería ser teóricamente equivalente al método de Simone, ya que utiliza la misma expresión regular internamente.


17
Lamentablemente 'http://'coincide con el patrón anterior. Ver:URI::regexp(%w(http https)) =~ 'http://'
David J.

15
También una url como http:fakeserá válida.
nathanvda

54

Siguiendo la idea de Simone, puede crear fácilmente su propio validador.

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

y luego usar

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

en tu modelo


1
¿Dónde debo poner esta clase? En un inicializador?
deb

3
Cito de @gbc: "Si coloca sus validadores personalizados en la aplicación / validadores, se cargarán automáticamente sin necesidad de alterar su archivo config / application.rb". ( stackoverflow.com/a/6610270/839847 ). Tenga en cuenta que la respuesta a continuación de Stefan Pettersson muestra que también guardó un archivo similar en "aplicaciones / validadores".
bergie3000

44
esto solo verifica si la url comienza con http: // o https: //, no es una validación de URL adecuada
maggix

1
Termine si puede permitirse que la URL sea opcional: class OptionalUrlValidator <UrlValidator def validate_each (record, attribute, value) return true if value.blank? return super end end
Dirty Henry

1
Esta no es una buena validación:URI("http:").kind_of?(URI::HTTP) #=> true
smathy

29

También hay validate_url gem (que es solo un buen contenedor para la Addressable::URI.parsesolución).

Solo agrega

gem 'validate_url'

a tu Gemfile, y luego en modelos puedes

validates :click_through_url, url: true

@ ЕвгенийМасленков que podría ser igual de bueno porque es válido de acuerdo con las especificaciones, pero es posible que desee consultar github.com/sporkmonger/addressable/issues . También en el caso general, hemos encontrado que nadie sigue el estándar y, en cambio, está utilizando una validación de formato simple.
dolzenko

13

Esta pregunta ya está respondida, pero qué diablos, propongo la solución que estoy usando.

La expresión regular funciona bien con todas las URL que he conocido. El método de establecimiento es tener cuidado si no se menciona ningún protocolo (supongamos que http: //).

Y finalmente, intentamos recuperar la página. Tal vez debería aceptar redireccionamientos y no 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

y...

# 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

realmente ordenado! gracias por su aporte, a menudo hay muchos enfoques para un problema; es genial cuando la gente comparte el suyo.
Jay

66
Solo quería señalar que, de acuerdo con la guía de seguridad de rieles , debe usar \ A y \ z en lugar de $ ^ en esa expresión regular
Jared

1
Me gusta. Sugerencia rápida para secar un poco el código moviendo la expresión regular al validador, ya que imagino que querrá que sea coherente en todos los modelos. Bonificación: le permitiría colocar la primera línea en validate_each.
Paul Pettengill

¿Qué pasa si la url está tomando mucho tiempo y tiempo de espera? ¿Cuál será la mejor opción para mostrar el mensaje de error de tiempo de espera o si no se puede abrir la página?
user588324

esto nunca pasaría una auditoría de seguridad, estás haciendo que tus servidores introduzcan una url arbitraria
Mauricio

12

También puede probar valid_url gem que permite URL sin el esquema, verifica la zona de dominio y los nombres de host de IP.

Agréguelo a su Gemfile:

gem 'valid_url'

Y luego en modelo:

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

Esto es muy bueno, especialmente las URL sin esquema, que sorprendentemente está involucrado con la clase URI.
Paul Pettengill

Me sorprendió la capacidad de esta gema para explorar las URL basadas en IP y detectar las falsas. ¡Gracias!
The Whiz of Oz

10

Solo mis 2 centavos:

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

EDITAR: expresión regular modificada para que coincida con las URL de los parámetros.


1
gracias por su aporte, siempre es bueno ver diferentes soluciones
jay

Por cierto, tu http://test.com/fdsfsdf?a=b
expresión regular

2
Pusimos este código en producción y seguimos obteniendo tiempos de espera en bucles infinitos en la línea .match regex. No estoy seguro de por qué, solo tenga cuidado con algunas esquinas y me encantaría escuchar los pensamientos de otros sobre por qué esto ocurriría.
toobulkeh

10

La solución que funcionó para mí fue:

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

Intenté usar algunos de los ejemplos que adjuntaste, pero estoy apoyando la URL de esta manera:

Observe el uso de A y Z porque si usa ^ y $ verá esta advertencia de seguridad de los validadores de 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
Intenta esto con "https://portal.example.com/portal/#". En Ruby 2.1.6 la evaluación se cuelga.
Old Pro

tienes razón parece que en algunos casos esta expresión regular tarda una eternidad en resolverse :(
heriberto perez

1
obviamente, no hay una expresión regular que cubra todos los escenarios, es por eso que termino usando solo una validación simple: valida: url, formato: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
heriberto perez

5

Me encontré con el mismo problema últimamente (necesitaba validar las URL en una aplicación Rails) pero tuve que hacer frente al requisito adicional de las URL unicode (p. Ej. http://кц.рф ) ...

Investigué un par de soluciones y encontré lo siguiente:


Sí, pero Addressable::URI.parse('http:///').scheme # => "http"o Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')están perfectamente bien desde el punto de vista de
Dirigible

4

Aquí hay una versión actualizada del validador publicado por David James . Ha sido publicado por Benjamin Fleischer . Mientras tanto, presioné un tenedor actualizado que se puede encontrar aquí .

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

Tenga en cuenta que todavía hay URI HTTP extraños que se analizan como direcciones válidas.

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

Aquí hay un problema para la addressablegema que cubre los ejemplos.


3

Utilizo una ligera variación en la solución lafeber anterior . No permite puntos consecutivos en el nombre de host (como por ejemplo en www.many...dots.com):

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

URI.parseparece exigir el prefijo de esquema, que en algunos casos no es lo que puede desear (por ejemplo, si desea permitir que sus usuarios deletreen rápidamente URL en formas como twitter.com/username)


2

He estado usando la gema 'activevalidators' y funciona bastante bien (no solo para la validación de URL)

puedes encontrarlo aquí

Todo está documentado, pero básicamente una vez que se agregó la gema, querrá agregar las siguientes líneas en un inicializador, por ejemplo: /config/environments/initializers/active_validators_activation.rb

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

(Nota: puede reemplazar: todo por: url o: lo que sea si solo desea validar tipos específicos de valores)

Y luego en tu modelo algo como esto

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

Ahora reinicie el servidor y eso debería ser


2

Si desea una validación simple y un mensaje de error personalizado:

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

1

Puede validar varias URL utilizando algo como:

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

1
¿Cómo manejaría las URL sin el esquema (por ejemplo, www.bar.com/foo)?
craig


1

Recientemente tuve este mismo problema y encontré una solución para las URL válidas.

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 primera parte del método validate_url es suficiente para validar el formato url. La segunda parte se asegurará de que la URL exista enviando una solicitud.


¿Qué pasa si la url apunta a un recurso que es muy grande (digamos, varios gigabytes)?
Jon Schneider

@JonSchneider uno podría usar una solicitud de encabezado http (como aquí ) en lugar de get.
wvengen

1

Me gustó monopatch el módulo URI para agregar el válido? método

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

Y como un 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

Y luego, include UrlValidatoren cualquier modelo para el que desee validar las URL. Solo incluye opciones.


0

La validación de URL no se puede manejar simplemente mediante el uso de una expresión regular, ya que el número de sitios web sigue creciendo y siguen apareciendo nuevos esquemas de nombres de dominio.

En mi caso, simplemente escribo un validador personalizado que busca una respuesta exitosa.

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

Estoy validando el pathatributo de mi modelo usando record.path. También estoy enviando el error al nombre del atributo respectivo usando record.errors[:path].

Simplemente puede reemplazar esto con cualquier nombre de atributo.

Luego, simplemente llamo al validador personalizado en mi modelo.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end

¿Qué pasa si la url apunta a un recurso que es muy grande (digamos, varios gigabytes)?
Jon Schneider

0

Podrías usar regex para esto, para mí funciona bien este:

(^|[\s.:;?\-\]<\(])(ftp|https?:\/\/[-\w;\/?:@&=+$\|\_.!~*\|'()\[\]%#,]+[\w\/#](\(\))?)(?=$|[\s',\|\(\).:;?\-\[\]>\)])
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.