Rails: quel est le bon moyen de valider les liens (URL)?

125

Je me demandais comment valider au mieux les URL dans Rails. Je pensais utiliser une expression régulière, mais je ne suis pas sûr que ce soit la meilleure pratique.

Et, si je devais utiliser une regex, quelqu'un pourrait-il m'en suggérer une? Je suis encore nouveau sur Regex.

geai
la source
En relation: stackoverflow.com/questions/1805761/…
Jon Schneider

Réponses:

151

La validation d'une URL est une tâche délicate. C'est aussi une demande très large.

Que voulez-vous faire exactement? Voulez-vous valider le format de l'URL, l'existence ou quoi? Il existe plusieurs possibilités, selon ce que vous souhaitez faire.

Une expression régulière peut valider le format de l'URL. Mais même une expression régulière complexe ne peut garantir que vous avez affaire à une URL valide.

Par exemple, si vous prenez une expression régulière simple, elle rejettera probablement l'hôte suivant

http://invalid##host.com

mais cela permettra

http://invalid-host.foo

c'est un hôte valide, mais pas un domaine valide si vous considérez les TLD existants. En effet, la solution fonctionnerait si vous souhaitez valider le nom d'hôte, pas le domaine car le suivant est un nom d'hôte valide

http://host.foo

ainsi que le suivant

http://localhost

Maintenant, laissez-moi vous donner quelques solutions.

Si vous souhaitez valider un domaine, vous devez oublier les expressions régulières. La meilleure solution disponible pour le moment est la Public Suffix List, une liste maintenue par Mozilla. J'ai créé une bibliothèque Ruby pour analyser et valider les domaines par rapport à la liste de suffixes publics, et elle s'appelle PublicSuffix .

Si vous souhaitez valider le format d'un URI / URL, vous pouvez utiliser des expressions régulières. Au lieu d'en rechercher un, utilisez la URI.parseméthode Ruby intégrée .

require 'uri'

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

Vous pouvez même décider de le rendre plus restrictif. Par exemple, si vous souhaitez que l'URL soit une URL HTTP / HTTPS, vous pouvez rendre la validation plus précise.

require 'uri'

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

Bien sûr, il existe des tonnes d'améliorations que vous pouvez appliquer à cette méthode, notamment la vérification d'un chemin ou d'un schéma.

Enfin, vous pouvez également regrouper ce code dans un validateur:

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
la source
1
Notez que le cours sera URI::HTTPSpour https uris (ex:URI.parse("https://yo.com").class => URI::HTTPS
tee
12
URI::HTTPShérite de URI:HTTP, c'est la raison pour laquelle j'utilise kind_of?.
Simone Carletti
1
De loin la solution la plus complète pour valider une URL en toute sécurité.
Fabrizio Regini
4
URI.parse('http://invalid-host.foo')renvoie true car cet URI est une URL valide. Notez également qu'il .foos'agit désormais d'un TLD valide. iana.org/domains/root/db/foo.html
Simone Carletti
1
@jmccartie veuillez lire l'intégralité du message. Si vous vous souciez du schéma, vous devez utiliser le code final qui inclut également une vérification de type, pas seulement cette ligne. Vous avez arrêté de lire avant la fin du message.
Simone Carletti
101

J'utilise une doublure à l'intérieur de mes modèles:

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

Je pense que c'est assez bon et simple à utiliser. De plus, elle devrait être théoriquement équivalente à la méthode de Simone, car elle utilise la même expression rationnelle en interne.

Matteo Collina
la source
17
Correspond malheureusement 'http://'au modèle ci-dessus. Voir:URI::regexp(%w(http https)) =~ 'http://'
David
15
Une URL comme celle-ci http:fakesera également valide.
nathanvda
54

Suivant l'idée de Simone, vous pouvez facilement créer votre propre validateur.

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

puis utilisez

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

dans votre modèle.

jlfenaux
la source
1
où dois-je mettre cette classe? Dans un initialiseur?
déb
3
Je cite @gbc: "Si vous placez vos validateurs personnalisés dans app / validators, ils seront automatiquement chargés sans avoir besoin de modifier votre fichier config / application.rb." ( stackoverflow.com/a/6610270/839847 ). Notez que la réponse ci-dessous de Stefan Pettersson montre qu'il a également enregistré un fichier similaire dans "app / validators".
bergie3000
4
cela ne vérifie que si l'URL commence par http: // ou https: //, ce n'est pas une validation d'URL correcte
maggix
1
Terminez si vous pouvez vous permettre que l'URL soit facultative: class OptionalUrlValidator <UrlValidator def validate_each (enregistrement, attribut, valeur) return true si value.blank? retour super fin fin
Dirty Henry
1
Ce n'est pas une bonne validation:URI("http:").kind_of?(URI::HTTP) #=> true
smathy
29

Il existe également une gemme validate_url (qui n'est qu'un bon wrapper pour la Addressable::URI.parsesolution).

Il suffit d'ajouter

gem 'validate_url'

à votre Gemfile, puis dans les modèles, vous pouvez

validates :click_through_url, url: true
Dolzenko
la source
@ ЕвгенийМасленков cela pourrait être tout aussi bien parce que c'est valide selon les spécifications, mais vous voudrez peut-être vérifier github.com/sporkmonger/addressable/issues . De plus, dans le cas général, nous avons constaté que personne ne suit la norme et utilise à la place une simple validation de format.
dolzenko
13

Cette question a déjà une réponse, mais bon sang, je propose la solution que j'utilise.

L'expression régulière fonctionne bien avec toutes les URL que j'ai rencontrées. La méthode setter est de faire attention si aucun protocole n'est mentionné (supposons http: //).

Et enfin, nous essayons de récupérer la page. Peut-être que je devrais accepter les redirections et pas seulement 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

et...

# 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
la source
vraiment chouette! merci pour votre contribution, il existe souvent de nombreuses approches à un problème; c'est super quand les gens partagent le leur.
geai
6
Je voulais juste souligner que selon le guide de sécurité des rails, vous devriez utiliser \ A et \ z plutôt que $ ^ dans cette expression rationnelle
Jared
1
Je l'aime. Suggestion rapide pour sécher un peu le code en déplaçant l'expression régulière dans le validateur, car j'imagine que vous voudriez qu'il soit cohérent entre les modèles. Bonus: cela vous permettrait de déposer la première ligne sous validate_each.
Paul Pettengill le
Que faire si l'URL prend du temps et expire? Quelle sera la meilleure option pour afficher le message d'erreur de délai d'expiration ou si la page ne peut pas être ouverte?
user588324
cela ne passerait jamais un audit de sécurité, vous obligez vos serveurs à piquer une URL arbitraire
Mauricio
12

Vous pouvez également essayer le gem valid_url qui autorise les URL sans le schéma, vérifie la zone de domaine et les noms d'hôte IP.

Ajoutez-le à votre Gemfile:

gem 'valid_url'

Et puis en modèle:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Roman Ralovets
la source
C'est tellement agréable, en particulier les URL sans schéma, ce qui est étonnamment impliqué avec la classe URI.
Paul Pettengill
J'ai été surpris par la capacité de ce joyau à fouiller dans les URL basées sur IP et à détecter les fausses. Merci!
The Whiz of Oz
10

Juste mes 2 cents:

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: modification de l'expression régulière pour correspondre aux URL des paramètres.

lafeber
la source
1
merci pour votre contribution, toujours bon de voir différentes solutions
jay
Btw, votre expression régulière rejettera les URL valides avec une chaîne de requête telle quehttp://test.com/fdsfsdf?a=b
MikDiet
2
Nous avons mis ce code en production et avons continué à obtenir des délais d'attente sur des boucles infinies sur la ligne de regex .match. Je ne sais pas pourquoi, faites juste attention à certaines majuscules et j'aimerais entendre les opinions des autres sur les raisons pour lesquelles cela se produirait.
toobulkeh
10

La solution qui a fonctionné pour moi était:

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

J'ai essayé d'utiliser certains des exemples que vous avez joints, mais je soutiens l'URL comme ceci:

Notez l'utilisation de A et Z car si vous utilisez ^ et $ vous verrez cet avertissement de sécurité des validateurs 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
la source
1
Essayez ceci avec "https://portal.example.com/portal/#". Dans Ruby 2.1.6, l'évaluation se bloque.
Old Pro
vous avez raison semble que dans certains cas, cette expression régulière prend une éternité à résoudre :(
heriberto perez
1
évidemment, il n'y a pas de regex qui couvre tous les scénarios, c'est pourquoi je finis par utiliser juste une simple validation: valide: url, format: {avec: URI.regexp}, si: Proc.new {| a | une.url.présente? }
heriberto perez
5

J'ai rencontré le même problème ces derniers temps (je devais valider les URL dans une application Rails) mais j'ai dû faire face à l'exigence supplémentaire d'urls unicode (par exemple http://кц.рф) ...

J'ai recherché quelques solutions et suis tombé sur les suivantes:

  • La première chose et la plus suggérée est l'utilisation URI.parse. Consultez la réponse de Simone Carletti pour plus de détails. Cela fonctionne bien, mais pas pour les URL Unicode.
  • La deuxième méthode que j'ai vue était celle d'Ilya Grigorik: http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/ Fondamentalement, il essaie de faire une demande au url; si ça marche, c'est valable ...
  • La troisième méthode que j'ai trouvée (et celle que je préfère) est une approche similaire à URI.parsemais en utilisant le addressablegem au lieu de la URIstdlib. Cette approche est détaillée ici: http://rawsyntax.com/blog/url-validation-in-rails-3-and-ruby-in-general/
Severin
la source
Ouais, mais Addressable::URI.parse('http:///').scheme # => "http"ou Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')sont parfaitement ok du point de vue d'
Adressable
4

Voici une version mise à jour du validateur publiée par David James . Il a été publié par Benjamin Fleischer . Pendant ce temps, j'ai poussé une fourchette mise à jour qui peut être trouvée ici .

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

Veuillez noter qu'il existe encore des URI HTTP étranges qui sont analysés comme des adresses valides.

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

Voici un numéro pour la addressablegemme qui couvre les exemples.

JJD
la source
3

J'utilise une légère variation sur la solution lafeber ci-dessus . Il interdit les points consécutifs dans le nom d'hôte (comme par exemple dans www.many...dots.com):

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

URI.parsesemble imposer un préfixe de schéma, ce qui dans certains cas n'est pas ce que vous voudrez peut-être (par exemple, si vous souhaitez permettre à vos utilisateurs d'épeler rapidement des URL dans des formulaires tels que twitter.com/username)

Franco
la source
2

J'utilise le bijou « activevalidators » et il est fonctionne assez bien ( et pas seulement pour la validation urls)

vous pouvez le trouver ici

Tout est documenté, mais une fois la gemme ajoutée, vous voudrez ajouter les quelques lignes suivantes dans un initialiseur, par exemple: /config/environments/initializers/active_validators_activation.rb

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

(Remarque: vous pouvez remplacer: all par: url ou: peu importe si vous souhaitez simplement valider des types de valeurs spécifiques)

Et puis de retour dans votre modèle quelque chose comme ça

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

Redémarrez maintenant le serveur et ça devrait être tout

Arnaud Bouchot
la source
2

Si vous souhaitez une validation simple et un message d'erreur personnalisé:

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

Vous pouvez valider plusieurs URL en utilisant quelque chose comme:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Damien Roche
la source
1
Comment géreriez-vous les URL sans le schéma (par exemple www.bar.com/foo)?
craig
1

Récemment, j'ai eu ce même problème et j'ai trouvé un moyen de contourner les URL valides.

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 première partie de la méthode validate_url suffit à valider le format de l'url. La deuxième partie s'assurera que l'URL existe en envoyant une requête.

Dilnavaz
la source
Que faire si l'URL pointe vers une ressource très volumineuse (par exemple, plusieurs gigaoctets)?
Jon Schneider
@JonSchneider on pourrait utiliser une requête http head (comme ici ) au lieu de get.
wvengen
1

J'ai aimé monkeypatch le module URI pour ajouter le valide? méthode

à l'intérieur 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
la source
0

Et en tant que module

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

Et puis juste include UrlValidatordans n'importe quel modèle pour lequel vous souhaitez valider les URL. Juste y compris pour les options.

MCB
la source
0

La validation d'URL ne peut pas être gérée simplement en utilisant une expression régulière car le nombre de sites Web ne cesse de croître et de nouveaux schémas de dénomination de domaine continuent à apparaître.

Dans mon cas, j'écris simplement un validateur personnalisé qui vérifie une réponse réussie.

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

Je valide l' pathattribut de mon modèle en utilisant record.path. Je pousse également l'erreur vers le nom d'attribut respectif en utilisant record.errors[:path].

Vous pouvez simplement le remplacer par n'importe quel nom d'attribut.

Ensuite, j'appelle simplement le validateur personnalisé dans mon modèle.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Noman Ur Rehman
la source
Que faire si l'URL pointe vers une ressource très volumineuse (par exemple, plusieurs gigaoctets)?
Jon Schneider
0

Vous pouvez utiliser regex pour cela, pour moi fonctionne bien celui-ci:

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