Rails: Apa cara yang baik untuk memvalidasi tautan (URL)?

125

Saya bertanya-tanya bagaimana cara terbaik memvalidasi URL di Rails. Saya berpikir untuk menggunakan ekspresi reguler, tetapi tidak yakin apakah ini adalah praktik terbaik.

Dan, jika saya menggunakan regex, dapatkah seseorang menyarankannya kepada saya? Saya masih baru di Regex.

jay
sumber

Jawaban:

151

Memvalidasi URL adalah pekerjaan yang rumit. Ini juga permintaan yang sangat luas.

Sebenarnya apa yang ingin kamu lakukan? Apakah Anda ingin memvalidasi format URL, keberadaannya, atau apa? Ada beberapa kemungkinan, tergantung pada apa yang ingin Anda lakukan.

Ekspresi reguler dapat memvalidasi format URL. Tetapi bahkan ekspresi reguler yang kompleks tidak dapat memastikan Anda berurusan dengan URL yang valid.

Misalnya, jika Anda menggunakan ekspresi reguler sederhana, host berikut mungkin akan ditolak

http://invalid##host.com

tapi itu akan memungkinkan

http://invalid-host.foo

itu adalah host yang valid, tetapi bukan domain yang valid jika Anda mempertimbangkan TLD yang ada. Memang, solusi akan berhasil jika Anda ingin memvalidasi nama host, bukan domainnya karena yang berikut ini adalah nama host yang valid

http://host.foo

serta yang berikut ini

http://localhost

Sekarang, izinkan saya memberi Anda beberapa solusi.

Jika Anda ingin memvalidasi domain, Anda harus melupakan ekspresi reguler. Solusi terbaik yang tersedia saat ini adalah Daftar Sufiks Publik, daftar yang dikelola oleh Mozilla. Saya membuat perpustakaan Ruby untuk mengurai dan memvalidasi domain terhadap Daftar Sufiks Publik, dan itu disebut PublicSuffix .

Jika Anda ingin memvalidasi format URI / URL, Anda mungkin ingin menggunakan ekspresi reguler. Alih-alih mencarinya, gunakan URI.parsemetode Ruby bawaan.

require 'uri'

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

Anda bahkan dapat memutuskan untuk membuatnya lebih ketat. Misalnya, jika Anda ingin URL menjadi HTTP / HTTPS URL, Anda dapat membuat validasi lebih akurat.

require 'uri'

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

Tentu saja, ada banyak perbaikan yang bisa Anda terapkan pada metode ini, termasuk memeriksa jalur atau skema.

Terakhir, Anda juga dapat mengemas kode ini menjadi validator:

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
sumber
1
Perhatikan bahwa kelas akan URI::HTTPSuntuk https uris (mis:URI.parse("https://yo.com").class => URI::HTTPS
tee
12
URI::HTTPSmewarisi dari URI:HTTP, itulah alasan mengapa saya menggunakan kind_of?.
Simone Carletti
1
Sejauh ini, solusi paling lengkap untuk memvalidasi URL dengan aman.
Fabrizio Regini
4
URI.parse('http://invalid-host.foo')mengembalikan nilai true karena URI tersebut adalah URL yang valid. Perhatikan juga bahwa .foosekarang menjadi TLD yang valid. iana.org/domains/root/db/foo.html
Simone Carletti
1
@jmccartie silahkan baca seluruh posting. Jika Anda peduli dengan skema, Anda harus menggunakan kode terakhir yang menyertakan juga pemeriksaan tipe, bukan hanya baris itu. Anda berhenti membaca sebelum akhir posting.
Simone Carletti
101

Saya menggunakan satu liner di dalam model saya:

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

Menurut saya cukup bagus dan mudah digunakan. Selain itu secara teoritis harus setara dengan metode Simone, karena menggunakan regexp yang sama secara internal.

Matteo Collina
sumber
17
Sayangnya 'http://'cocok dengan pola di atas. Lihat:URI::regexp(%w(http https)) =~ 'http://'
David J.
15
Juga url seperti http:fakeakan valid.
nathanvda
54

Mengikuti ide Simone, Anda dapat dengan mudah membuat validator sendiri.

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

dan kemudian gunakan

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

dalam model Anda.

jlfenaux
sumber
1
di mana saya harus meletakkan kelas ini? Di penginisialisasi?
deb
3
Saya mengutip dari @gbc: "Jika Anda menempatkan validator kustom Anda di app / validator, validator tersebut akan dimuat secara otomatis tanpa perlu mengubah file config / application.rb Anda." ( stackoverflow.com/a/6610270/839847 ). Perhatikan bahwa jawaban di bawah dari Stefan Pettersson menunjukkan bahwa dia juga menyimpan file serupa di "app / validators".
bergie3000
4
ini hanya memeriksa apakah url dimulai dengan http: // atau https: //, ini bukan validasi URL yang tepat
maggix
1
Akhiri jika Anda dapat mengubah URL menjadi opsional: class OpsionalUrlValidator <UrlValidator def validate_each (record, attribute, value) return true if value.blank? kembali super end end
Dirty Henry
1
Ini bukan validasi yang baik:URI("http:").kind_of?(URI::HTTP) #=> true
smathy
29

Ada juga permata validate_url (yang merupakan pembungkus yang bagus untukAddressable::URI.parse solusi).

Tambahkan saja

gem 'validate_url'

ke Anda Gemfile, dan kemudian dalam model yang Anda bisa

validates :click_through_url, url: true
dolzenko
sumber
@ ЕвгенийМасленков yang mungkin sama baiknya karena valid menurut spesifikasi, tetapi Anda mungkin ingin memeriksa github.com/sporkmonger/addressable/issues . Juga dalam kasus umum kami menemukan bahwa tidak ada yang mengikuti standar dan sebaliknya menggunakan validasi format sederhana.
dolzenko
13

Pertanyaan ini sudah terjawab, tapi apa sih, saya mengusulkan solusi yang saya gunakan.

Regexp berfungsi dengan baik dengan semua url yang saya temui. Metode penyetel adalah berhati-hati jika tidak ada protokol yang disebutkan (anggap saja http: //).

Dan akhirnya, kami mencoba mengambil halaman tersebut. Mungkin saya harus menerima pengalihan dan tidak hanya 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

dan...

# 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
sumber
sangat rapi! terima kasih atas masukan Anda, seringkali ada banyak pendekatan untuk suatu masalah; itu bagus ketika orang membagikan milik mereka.
jay
6
Hanya ingin menunjukkan bahwa menurut panduan keamanan rel, Anda harus menggunakan \ A dan \ z daripada $ ^ dalam regexp itu
Jared
1
Saya suka itu. Saran cepat untuk mengeringkan kode sedikit dengan memindahkan regex ke validator, seperti yang saya bayangkan Anda ingin konsisten di seluruh model. Bonus: Ini akan memungkinkan Anda untuk meletakkan baris pertama di bawah validate_each.
Paul Pettengill
Bagaimana jika url membutuhkan waktu lama dan waktu tunggu? Apa pilihan terbaik untuk menampilkan pesan kesalahan batas waktu atau jika halaman tidak dapat dibuka?
pengguna588324
ini tidak akan lolos audit keamanan, Anda membuat server Anda mencolek url sewenang-wenang
Mauricio
12

Anda juga dapat mencoba valid_url gem yang memungkinkan URL tanpa skema, memeriksa zona domain dan nama host ip.

Tambahkan ke Gemfile Anda:

gem 'valid_url'

Dan kemudian di model:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end
Roman Ralovets
sumber
Ini sangat bagus, terutama URL tanpa skema, yang secara mengejutkan terlibat dengan kelas URI.
Paul Pettengill
Saya terkejut dengan kemampuan permata ini untuk menggali melalui URL berbasis IP dan mendeteksi yang palsu. Terima kasih!
The Whiz of Oz
10

Hanya 2 sen saya:

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: mengubah ekspresi reguler untuk mencocokkan url parameter.

lafeber
sumber
1
terima kasih atas masukan Anda, selalu senang melihat solusi yang berbeda
jay
Btw, regexp Anda akan menolak url valid dengan string kueri sepertihttp://test.com/fdsfsdf?a=b
MikDiet
2
Kami memasukkan kode ini ke dalam produksi dan terus mendapatkan waktu tunggu pada loop tak terbatas di baris regex .match. Tidak yakin mengapa, berhati-hatilah untuk beberapa kasus cornercases dan ingin mendengar pendapat orang lain tentang mengapa hal ini bisa terjadi.
toobulkeh
10

Solusi yang berhasil untuk saya adalah:

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

Saya memang mencoba menggunakan beberapa contoh yang Anda lampirkan tetapi saya mendukung url seperti:

Perhatikan penggunaan A dan Z karena jika Anda menggunakan ^ dan $ Anda akan melihat peringatan keamanan ini dari validator 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
sumber
1
Coba ini dengan "https://portal.example.com/portal/#". Di Ruby 2.1.6 evaluasi macet.
Old Pro
Anda benar sepertinya dalam beberapa kasus ekspresi reguler ini membutuhkan waktu lama untuk diselesaikan :(
heriberto perez
1
jelas, tidak ada regex yang mencakup setiap skenario, itulah mengapa saya akhirnya hanya menggunakan validasi sederhana: validates: url, format: {with: URI.regexp}, if: Proc.new {| a | a.url.present? }
heriberto perez
5

Saya mengalami masalah yang sama belakangan ini (saya perlu memvalidasi url di aplikasi Rails) tetapi saya harus mengatasi persyaratan tambahan url unicode (mis. http://кц.рф ) ...

Saya meneliti beberapa solusi dan menemukan yang berikut:

severin
sumber
Ya, tapi Addressable::URI.parse('http:///').scheme # => "http"atau Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')sangat oke dari sudut pandang Addressable :(
smileart
4

Berikut adalah versi terbaru dari validator yang diposting oleh David James . Ini telah diterbitkan oleh Benjamin Fleischer . Sementara itu, saya mendorong garpu yang diperbarui yang dapat ditemukan di sini .

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

Harap perhatikan bahwa masih ada URI HTTP aneh yang diuraikan sebagai alamat yang valid.

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

Ini adalah masalah addressablepermata yang mencakup contoh.

JJD
sumber
3

Saya menggunakan sedikit variasi pada solusi lafeber di atas . Ini melarang titik berurutan di nama host (seperti misalnya di www.many...dots.com):

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

URI.parsetampaknya mengamanatkan skema awalan, yang dalam beberapa kasus bukan yang Anda inginkan (misalnya jika Anda ingin mengizinkan pengguna Anda untuk mengeja URL dengan cepat dalam bentuk seperti twitter.com/username)

Franco
sumber
2

Saya telah menggunakan permata 'activevalidators' dan berfungsi dengan cukup baik (tidak hanya untuk validasi url)

Anda dapat menemukannya di sini

Semuanya didokumentasikan tetapi pada dasarnya setelah permata ditambahkan, Anda akan ingin menambahkan beberapa baris berikut di penginisialisasi, katakan: /config/environments/initializers/active_validators_activation.rb

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

(Catatan: Anda dapat mengganti: semua dengan: url atau: apa pun jika Anda hanya ingin memvalidasi jenis nilai tertentu)

Dan kemudian kembali ke model Anda sesuatu seperti ini

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

Sekarang Restart server dan seharusnya itu saja

Arnaud Bouchot
sumber
2

Jika Anda menginginkan validasi sederhana dan pesan kesalahan khusus:

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

Anda dapat memvalidasi banyak url menggunakan sesuatu seperti:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true
Damien Roche
sumber
1
Bagaimana Anda menangani URL tanpa skema (misalnya www.bar.com/foo)?
craig
1

Baru-baru ini saya mengalami masalah yang sama dan saya menemukan solusi untuk url yang valid.

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

Bagian pertama dari metode validate_url sudah cukup untuk memvalidasi format url. Bagian kedua akan memastikan url ada dengan mengirimkan permintaan.

Dilnavaz
sumber
Bagaimana jika url mengarah ke sumber daya yang sangat besar (misalnya, beberapa gigabyte)?
Jon Schneider
@JonSider seseorang dapat menggunakan permintaan kepala http (seperti di sini ) daripada get.
wvengen
1

Saya suka mencocokkan modul URI untuk menambahkan valid? metode

dalam 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
sumber
0

Dan sebagai modul

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

Dan kemudian hanya include UrlValidatordi model apa pun yang ingin Anda validasi url-nya. Hanya termasuk untuk opsi.

MCB
sumber
0

Validasi URL tidak dapat ditangani hanya dengan menggunakan Ekspresi Reguler karena jumlah situs web terus bertambah dan skema penamaan domain baru terus bermunculan.

Dalam kasus saya, saya hanya menulis validator khusus yang memeriksa respons yang berhasil.

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

Saya memvalidasi pathatribut model saya dengan menggunakan record.path. Saya juga mendorong kesalahan ke nama atribut masing-masing dengan menggunakanrecord.errors[:path] .

Anda cukup mengganti ini dengan nama atribut apa saja.

Kemudian, saya cukup memanggil validator khusus dalam model saya.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end
Noman Ur Rehman
sumber
Bagaimana jika url mengarah ke sumber daya yang sangat besar (misalnya, beberapa gigabyte)?
Jon Schneider
0

Anda bisa menggunakan regex untuk ini, bagi saya berfungsi dengan baik yang ini:

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