Rails: Qual é uma boa maneira de validar links (URLs)?

125

Eu queria saber como validar melhor os URLs no Rails. Eu estava pensando em usar uma expressão regular, mas não tenho certeza se essa é a melhor prática.

E, se eu usasse uma regex, alguém poderia me sugerir uma? Eu ainda sou novo no Regex.

Jay
fonte
Veja também: stackoverflow.com/questions/1805761/…
Jon Schneider

Respostas:

151

Validar um URL é um trabalho complicado. Também é uma solicitação muito ampla.

O que você quer fazer exatamente? Deseja validar o formato da URL, a existência ou o quê? Existem várias possibilidades, dependendo do que você deseja fazer.

Uma expressão regular pode validar o formato do URL. Mas mesmo uma expressão regular complexa não pode garantir que você esteja lidando com um URL válido.

Por exemplo, se você usar uma expressão regular simples, ela provavelmente rejeitará o seguinte host

http://invalid##host.com

mas permitirá

http://invalid-host.foo

esse é um host válido, mas não um domínio válido se você considerar os TLDs existentes. De fato, a solução funcionaria se você deseja validar o nome do host, não o domínio, porque o seguinte é um nome de host válido

http://host.foo

bem o seguinte

http://localhost

Agora, deixe-me dar algumas soluções.

Se você deseja validar um domínio, precisa esquecer as expressões regulares. A melhor solução disponível no momento é a Public Suffix List, uma lista mantida pela Mozilla. Criei uma biblioteca Ruby para analisar e validar domínios com relação à lista pública de sufixos, chamada PublicSuffix .

Se você deseja validar o formato de um URI / URL, convém usar expressões regulares. Em vez de procurar um, use o URI.parsemétodo Ruby interno.

require 'uri'

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

Você pode até decidir torná-lo mais restritivo. Por exemplo, se você quiser que o URL seja um URL HTTP / HTTPS, poderá tornar a validação mais precisa.

require 'uri'

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

Obviamente, existem inúmeras melhorias que você pode aplicar a esse método, incluindo a verificação de um caminho ou esquema.

Por último, mas não menos importante, você também pode empacotar esse código em um 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
Simone Carletti
fonte
1
Observe que a classe será URI::HTTPSpara https uris (ex:URI.parse("https://yo.com").class => URI::HTTPS
tee
12
URI::HTTPSherda URI:HTTP, é por isso que eu uso kind_of?.
Simone Carletti
1
De longe, a solução mais completa para validar com segurança um URL.
Fabrizio Regini
4
URI.parse('http://invalid-host.foo')retorna true porque esse URI é um URL válido. Observe também que .fooagora é um TLD válido. iana.org/domains/root/db/foo.html
Simone Carletti
1
@jmccartie, leia o post inteiro. Se você se importa com o esquema, deve usar o código final que inclui também uma verificação de tipo, não apenas essa linha. Você parou de ler antes do final da postagem.
Simone Carletti
101

Eu uso um liner dentro dos meus modelos:

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

Eu acho que é bom o suficiente e simples de usar. Além disso, deveria ser teoricamente equivalente ao método de Simone, pois usa o mesmo regexp internamente.

Matteo Collina
fonte
17
Infelizmente 'http://'corresponde ao padrão acima. Veja:URI::regexp(%w(http https)) =~ 'http://'
David J.
15
Também um URL como http:fakeserá válido.
Nathanvda 11/11/12
54

Seguindo a ideia de Simone, você pode criar facilmente seu próprio 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

e depois use

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

no seu modelo.

jlfenaux
fonte
1
onde devo colocar essa aula? Em um inicializador?
deb
3
Cito @gbc: "Se você colocar seus validadores personalizados em app / validators, eles serão carregados automaticamente sem a necessidade de alterar seu arquivo config / application.rb." ( stackoverflow.com/a/6610270/839847 ). Observe que a resposta abaixo de Stefan Pettersson mostra que ele salvou um arquivo semelhante em "app / validators" também.
bergie3000
4
isso só verifica se o início url com http: // ou https: //, não é uma validação URL correta
maggix
1
Termine se você puder permitir que o URL seja opcional: class OptionalUrlValidator <UrlValidator def validate_each (registro, atributo, valor) retorne true se value.blank? return super end end #
Dirty Henry
1
Esta não é uma boa validação:URI("http:").kind_of?(URI::HTTP) #=> true
smathy 13/04
29

Também há uma validate_url gem (que é apenas um bom invólucro para a Addressable::URI.parsesolução).

Basta adicionar

gem 'validate_url'

ao seu Gemfilee, em modelos, você pode

validates :click_through_url, url: true
dolzenko
fonte
@ ЕвгенийМасленков que pode ser tão bom porque é válido de acordo com as especificações, mas você pode verificar github.com/sporkmonger/addressable/issues . Também no caso geral, descobrimos que ninguém segue o padrão e está usando validação de formato simples.
amigos estão dizendo sobre dolzenko
13

Esta pergunta já está respondida, mas que diabos, proponho a solução que estou usando.

O regexp funciona bem com todos os URLs que conheci. O método setter é tomar cuidado se nenhum protocolo for mencionado (vamos assumir http: //).

E, finalmente, tentamos buscar a página. Talvez eu deva aceitar redirecionamentos e não apenas 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
Stefan Pettersson
fonte
muito legal! obrigado pela sua contribuição, muitas vezes existem muitas abordagens para um problema; é ótimo quando as pessoas compartilham deles.
jay
6
Só queria salientar que, segundo a trilhos de guia de segurança que você deve usar \ A e \ z em vez de $ ^ em que regexp
Jared
1
Eu gosto disso. Sugestão rápida para secar um pouco o código movendo o regex para o validador, como eu imagino que você queira que ele seja consistente entre os modelos. Bônus: permitiria soltar a primeira linha em validate_each.
Paul Pettengill
E se o URL estiver demorando muito tempo? Qual será a melhor opção para mostrar a mensagem de erro de tempo limite ou se a página não puder ser aberta?
user588324
isso nunca passaria em uma auditoria de segurança, você está fazendo seus servidores cutucarem um URL arbitrário
Mauricio
12

Você também pode tentar a gem valid_url, que permite URLs sem o esquema, verifica a zona do domínio e os nomes de host IP.

Adicione-o ao seu Gemfile:

gem 'valid_url'

E então no modelo:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Roman Ralovets
fonte
Isso é muito bom, especialmente os URLs sem esquema, que estão surpreendentemente envolvidos com a classe URI.
Paul Pettengill
Fiquei surpreso com a capacidade dessa gema de explorar URLs baseadas em IP e detectar as falsas. Obrigado!
The Whizz of Oz
10

Apenas meus 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

EDIT: mudou a regex para corresponder aos URLs dos parâmetros.

Lafeber
fonte
1
obrigado pela sua contribuição, sempre bom ver soluções diferentes #
jay
Btw, o seu regexp rejeitará URLs válidos com string de consulta como:http://test.com/fdsfsdf?a=b
MikDiet 26/02/15
2
Colocamos esse código em produção e continuamos recebendo tempos limite em infinitos loops na linha regmat .match. Não sei por que, apenas tome cuidado com alguns casos e gostaria de ouvir os pensamentos de outros sobre o motivo disso acontecer.
toobulkeh
10

A solução que funcionou para mim foi:

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

Eu tentei usar alguns dos exemplos anexados, mas estou suportando o URL da seguinte forma:

Observe o uso de A e Z porque se você usar ^ e $, verá este aviso de segurança dos validadores do 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
fonte
1
Tente isso com "https://portal.example.com/portal/#". No Ruby 2.1.6, a avaliação trava.
Old Pro
você está certo parece que em alguns casos esta expressão regular leva uma eternidade para resolver :(
heriberto perez
1
obviamente, não existe uma regex que cubra todos os cenários; é por isso que acabo usando apenas uma validação simples: valida: url, formato: {com: URI.regexp}, se: Proc.new {| a | a.url.present? }
heriberto perez 14/04
5

Ultimamente, encontrei o mesmo problema (eu precisava validar URLs em um aplicativo Rails), mas precisava atender aos requisitos adicionais de URLs unicode (por exemplo http://кц.рф) ...

Eu pesquisei algumas soluções e me deparei com o seguinte:

severin
fonte
Sim, mas Addressable::URI.parse('http:///').scheme # => "http"ou Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')são perfeitamente ok do ponto de vista :( do endereçável
smileart
4

Aqui está uma versão atualizada do validador postada por David James . Foi publicado por Benjamin Fleischer . Enquanto isso, empurrei um fork atualizado, que pode ser encontrado aqui .

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

Observe que ainda existem URIs HTTP estranhos que são analisados ​​como endereços válidos.

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

Aqui está um problema para a addressablegema que cobre os exemplos.

JJD
fonte
3

Eu uso uma ligeira variação na solução Lafeber acima . Não permite pontos consecutivos no nome do host (como por exemplo em www.many...dots.com):

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

URI.parseparece exigir a prefixação do esquema, que em alguns casos não é o que você deseja (por exemplo, se você deseja permitir que seus usuários escrevam rapidamente URLs em formas como twitter.com/username)

Franco
fonte
2

Eu tenho usado a gema 'activevalidators' e funciona muito bem (não apenas para validação de URLs)

você pode encontrá-lo aqui

Está tudo documentado, mas basicamente, uma vez adicionada a gema, você adicionará as seguintes linhas em um inicializador: /config/environments/initializers/active_validators_activation.rb

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

(Nota: você pode substituir: all por: url ou: what, se você quiser apenas validar tipos específicos de valores)

E então, de volta ao seu modelo, algo como isto

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

Agora reinicie o servidor e deve ser

Arnaud Bouchot
fonte
2

Se você deseja uma validação simples e uma mensagem de erro personalizada:

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

Você pode validar vários URLs usando algo como:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Damien Roche
fonte
1
Como você lidaria com URLs sem o esquema (por exemplo, www.bar.com/foo)?
craig
1

Recentemente, tive esse mesmo problema e encontrei uma solução alternativa para URLs válidos.

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

A primeira parte do método validate_url é suficiente para validar o formato da URL. A segunda parte garantirá que o URL exista enviando uma solicitação.

Dilnavaz
fonte
E se o URL apontar para um recurso muito grande (por exemplo, vários gigabytes)?
21816 Jon Schneider
@JonSchneider pode-se usar uma requisição http head (como aqui ) em vez de get.
wvengen
1

Eu gostava de monkeypatch o módulo URI para adicionar o 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
Blair Anderson
fonte
0

E como um módulo

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, em seguida, apenas include UrlValidatorem qualquer modelo para o qual você deseja validar os URLs. Apenas incluindo opções.

MCB
fonte
0

A validação de URL não pode ser gerenciada simplesmente usando uma Expressão regular, pois o número de sites continua crescendo e novos esquemas de nomeação de domínio continuam aparecendo.

No meu caso, simplesmente escrevo um validador personalizado que verifica se há uma resposta bem-sucedida.

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

Estou validando o pathatributo do meu modelo usando record.path. Também estou enviando o erro para o respectivo nome de atributo usando record.errors[:path].

Você pode simplesmente substituir isso por qualquer nome de atributo.

Depois, simplesmente chamo o validador personalizado no meu modelo.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Noman Ur Rehman
fonte
E se o URL apontar para um recurso muito grande (por exemplo, vários gigabytes)?
John Schneider
0

Você pode usar regex para isso, para mim funciona bem este:

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