web-dev-qa-db-fra.com

Rails: Quel est le bon moyen de valider des liens (URL)?

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.

119
jay

Valider 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 traitez avec 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 voulez valider le nom d’hôte, et non 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 à l'heure actuelle est la liste de suffixes publics, 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. 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 commande intégrée Ruby URI.parse méthode.

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 alors 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, vous pouvez appliquer de nombreuses améliorations à cette méthode, notamment la vérification d'un chemin ou d'un schéma.

Enfin et surtout, vous pouvez également intégrer 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
135
Simone Carletti

J'utilise une doublure dans mes modèles:

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

Je pense 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.

98
Matteo Collina

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

et ensuite utiliser

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

dans votre modèle.

52
jlfenaux

Il y a aussi validate_url gem (qui est juste un wrapper de Nice pour Addressable::URI.parse Solution).

Ajoutez simplement

gem 'validate_url'

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

validates :click_through_url, url: true
26
dolzenko

On a déjà répondu à cette question, mais bon sang, je propose la solution que j'utilise.

L'expression rationnelle fonctionne bien avec toutes les URL que j'ai rencontrées. La méthode de définition est à prendre en compte si aucun protocole n'est mentionné (supposons http: //).

Et finalement, nous essayons de 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'

# 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
14

Vous pouvez également essayer valid_url gem, 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 dans le modèle:

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

Juste mes 2 centimes:

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 rationnelle pour qu'elle corresponde aux URL du paramètre.

10
lafeber

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 j'appuie l'URL comme suit:

Notez l'utilisation de A et Z car si vous utilisez ^ et $, vous verrez cet avertissement de sécurité de Rails validateurs.

 Valid ones:
 'www.crowdint.com'
 'crowdint.com'
 'http://crowdint.com'
 'http://www.crowdint.com'

 Invalid ones:
  'http://www.crowdint. com'
  'http://fake'
  'http:fake'
10
heriberto perez

J'ai rencontré le même problème récemment (je devais valider les URL dans un Rails app) mais je devais faire face à l'exigence supplémentaire d'URL Unicode (par exemple, http://кц.рф) ...

J'ai recherché quelques solutions et suis tombé sur les éléments suivants:

5
severin

Voici une version mise à jour du validateur posté par David James . Il a été publié par Benjamin Fleischer . En attendant, 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 toujours 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 le addressable gem qui couvre les exemples.

4
JJD

J'utilise une légère variation sur solution de 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.parse semble exiger le préfixage de schéma, ce qui dans certains cas n’est pas ce que vous souhaitiez (par exemple, si vous souhaitez autoriser vos utilisateurs à épeler rapidement des URL dans des formulaires tels que Twitter.com/username)

3
Franco

J'utilise la gemme 'activevalidators' et elle fonctionne plutôt bien (pas seulement pour la validation des URL)

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: tout par: url ou: peu importe si vous souhaitez simplement valider des types de valeurs spécifiques)

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

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

Maintenant Redémarrez le serveur et cela devrait être le cas

2
Arnaud Bouchot

Vous pouvez valider plusieurs URL en utilisant quelque chose comme:

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

Récemment, j'ai eu le 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 est suffisante pour valider le format url. La deuxième partie s'assurera que l'URL existe en envoyant une demande.

1
Dilnavaz

https://github.com/perfectline/validates_url est un joyau simple et agréable qui fera à peu près tout pour vous

1
stuartchaney

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'
            }
1
Caleb

Vous pouvez utiliser regex pour cela, pour moi ça marche bien celui-ci:

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

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

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

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'attribut path 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 remplacer ceci 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
0
Noman Ur Rehman

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
0
Blair Anderson

Et comme 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 UrlValidator dans n'importe quel modèle pour lequel vous souhaitez valider l'URL. Y compris juste pour les options.

0
MCB