145 votes

Rails : Quel est un bon moyen de valider les liens (URL) ?

Je me demandais comment je pourrais mieux valider les URL en 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 novice en Regex.

0 votes

167voto

Simone Carletti Points 77653

La validation d'une URL est un travail délicat. 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 y a plusieurs possibilités, selon ce que vous voulez faire.

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

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

http://invalid##host.com

mais elle permettra

http://invalid-host.foo

qui est un hôte valide, mais pas un domaine valide si l'on considère les TLD existants. En effet, la solution fonctionnerait si vous voulez 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 voulez valider un domaine, alors vous devez oublier les expressions régulières. La meilleure solution disponible actuellement 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 Public Suffix List, et elle s'appelle PublicSuffix.

Si vous voulez valider le format d'un URI/URL, alors vous voudrez peut-être utiliser des expressions régulières. Au lieu d'en rechercher une, utilisez la méthode Ruby intégrée URI.parse.

require 'uri'

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

Vous pouvez même décider de le rendre plus restrictif. Par exemple, si vous voulez 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.present?
rescue URI::InvalidURIError
  false

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

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

class HttpUrlValidator < ActiveModel::EachValidator

  def self.compliant?(value)
    uri = URI.parse(value)
    uri.is_a?(URI::HTTP) && uri.host.present?
  rescue URI::InvalidURIError
    false
  end

  def validate_each(record, attribute, value)
    unless value.present? && self.class.compliant?(value)
      record.errors.add(attribute, "n'est pas une URL HTTP valide")
    end
  end

end

# dans le modèle
validates :example_attribute, http_url: true

Note pour les nouvelles versions d'URI (c'est-à-dire 0.12.1)

.present? / .blank? serait un moyen plus précis de valider les hôtes, au lieu d'utiliser uri.host.nil? ou simplement if uri.host auparavant (c'est-à-dire URI v 0.11).

Exemple pour URI.parse("https:///394") :

  • Nouvelle version d'URI (0.12), host renverra une chaîne vide, et /394 deviendra un chemin. #
  • Ancienne version d'URI (0.11), host renverra une chaîne vide, et /394 deviendra également un chemin. #

1 votes

Notez que la classe sera URI::HTTPS pour les URI https (par ex : URI.parse("https://yo.com").class => URI::HTTPS

14 votes

URI::HTTPS hérite de URI:HTTP, c'est la raison pour laquelle j'utilise kind_of?.

1 votes

De loin la solution la plus complète pour valider en toute sécurité une URL.

115voto

Matteo Collina Points 649

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

validates :url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])

Je pense que c'est suffisant et simple à utiliser. De plus, cela devrait être théoriquement équivalent à la méthode de Simone, car elle utilise le même regex en interne.

17 votes

Malheureusement 'http://' correspond au motif ci-dessus. Voir: URI::regexp(%w(http https)) =~ 'http://'

17 votes

Aussi une URL comme http:fake sera valide.

56voto

jlfenaux Points 1413

En 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] || "n'est pas une URL")
    end
  end
end

puis utilisez

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

dans votre modèle.

1 votes

Où dois-je mettre cette classe? Dans un initialiseur?

3 votes

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 sauvegardé un fichier similaire dans "app/validators".

0 votes

Est-il possible de modifier ceci pour permettre aux protocoles d'être facultatifs?

31voto

dolzenko Points 2098

Il y a aussi le gem validate_url (qui est simplement un bel emballage pour la solution Addressable::URI.parse).

Ajoutez simplement

gem 'validate_url'

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

validates :click_through_url, url: true

0 votes

@ cela pourrait tout aussi bien être valide selon la spécification, mais vous voudrez peut-être vérifier github.com/sporkmonger/addressable/issues. Aussi dans le cas général, nous avons constaté que personne ne suit la norme et préfère utiliser une simple validation de format.

13voto

Stefan Pettersson Points 345

Cette question est déjà répondue, mais peu importe, 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 pour prendre en charge si aucun protocole n'est mentionné (supposons http://).

Enfin, nous faisons un essai pour récupérer la page. Peut-être devrais-je 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'

# Merci Ilya ! http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/
# Crédits originaux: http://blog.inquirylabs.com/2006/04/13/simple-uri-validation/
# Codes HTTP: 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, "Une expression régulière doit être fournie comme option :format du hash d'options") 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 # vérifier la réponse de l'en-tête
        case Net::HTTP.get_response(URI.parse(value))
          when Net::HTTPSuccess then true
          else object.errors.add(attribute, configuration[:message]) and false
        end
      rescue # Récupérer en cas d'échec DNS...
        object.errors.add(attribute, configuration[:message]) and false
      end
    else
      object.errors.add(attribute, configuration[:message]) and false
    end
  end
end

0 votes

Vraiment propre! merci pour votre contribution, il y a souvent plusieurs approches à un problème; c'est formidable lorsque les gens partagent les leurs.

6 votes

Je voulais juste souligner qu' selon le guide de sécurité de rails, vous devriez utiliser \A et \z plutôt que $^ dans cette expression rationnelle.

1 votes

J'aime ça. Suggestion rapide pour alléger un peu le code en déplaçant l'expression régulière dans le validateur, car je suppose que vous voulez qu'elle soit cohérente entre les modèles. Bonus : Cela vous permettrait de supprimer la première ligne sous validate_each.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X