How to: Validate the URL for remote uploads

ksaynice edited this page Sep 4, 2012 · 6 revisions
Clone this wiki locally

THIS PAGE IS A WORK IN PROGRESS

In Rails 3, you can add a custom validator class to validate that a URL is valid, both in terms of format (using a Regexp matcher) and that it resolves properly. The following example uses the code from https://gist.github.com/948880

First, create a new validator file and save it in lib/uri_validator.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 => "is invalid or not responding", :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

Then add the following line to either the top of your model file (if you will only need it in that one model) or your environment.rb file if you will need it in multiple models.

require 'uri_validator'

Finally, add the validator to your model.

class User < ActiveRecord::Base
  mount_uploader :avatar, AvatarUploader
  attr_accessor :remote_avatar_url
  # ...
  validates :remote_avatar_url, 
    :uri => { 
      :format => /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix,
      :unless => remote_avatar_url.blank?
    }
end

Testing

You can use the FakeWeb gem in your tests to temporarily message the response validation, for instance to ensure that that bit passes if you can't guarantee that the URL you are using in your test objects is valid.

# spec/spec_helper.rb
require 'FakeWeb'
# spec/models/user_spec.rb
describe User do
  before do
    uri = "http://no.good"
    FakeWeb.register_uri(:get, uri, :body => "OK")
    @user = User.create(:username => "Bob", :remote_avatar_url => uri)
  end
end

Here is an example using factory_girl and the FFaker gem:

# spec/factories.rb
Factory.define :user do |f|
  f.username { Faker::Internet.user_name }
  f.remote_avatar_url { Faker::Internet.http_url }

  f.after_build { |f| FakeWeb.register_uri(:get, f.remote_avatar_url, :body => "OK") }
end