user393964
user393964

Reputation:

Add http(s) to URL if it's not there?

I'm using this regex in my model to validate an URL submitted by the user. I don't want to force the user to type the http part, but would like to add it myself if it's not there.

validates :url, :format => { :with => /^((http|https):\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+).[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, :message => " is not valid" }

Any idea how I could do that? I have very little experience with validation and regex..

Upvotes: 38

Views: 22736

Answers (8)

joeyk16
joeyk16

Reputation: 1415

I had to do it for multiple columns on the same model.

  before_validation :add_url_protocol

  def add_url_protocol
    [
      :facebook_url, :instagram_url, :linkedin_url,
      :tiktok_url, :youtube_url, :twitter_url, :twitch_url
    ].each do |url_method|
      url = self.send(url_method)

      if url.present? && !(%w{http https}.include?(URI.parse(url).scheme))
        self.send("#{url_method.to_s}=", 'https://'.concat(url)) 
      end
    end
  end

Upvotes: 1

Douglas F Shearer
Douglas F Shearer

Reputation: 26488

Use a before filter to add it if it is not there:

before_validation :smart_add_url_protocol

protected

def smart_add_url_protocol
  unless url[/\Ahttp:\/\//] || url[/\Ahttps:\/\//]
    self.url = "http://#{url}"
  end
end

Leave the validation you have in, that way if they make a typo they can correct the protocol.

Upvotes: 74

mu is too short
mu is too short

Reputation: 434665

Don't do this with a regex, use URI.parse to pull it apart and then see if there is a scheme on the URL:

u = URI.parse('/pancakes')
if(!u.scheme)
  # prepend http:// and try again
elsif(%w{http https}.include?(u.scheme))
  # you're okay
else
  # you've been give some other kind of
  # URL and might want to complain about it
end

Using the URI library for this also makes it easy to clean up any stray nonsense (such as userinfo) that someone might try to put into a URL.

Upvotes: 43

Caleb
Caleb

Reputation: 3802

Using some of the aforementioned regexps, here is a handy method for overriding the default url on a model (If your ActiveRecord model has an 'url' column, for instance)

def url
  _url = read_attribute(:url).try(:downcase)
  if(_url.present?)
    unless _url[/\Ahttp:\/\//] || _url[/\Ahttps:\/\//]
      _url = "http://#{_url}"
    end
  end
_url
end

Upvotes: 2

Timo
Timo

Reputation: 3404

Preface, justification and how it should be done

I hate it when people change model in a before_validation hook. Then when someday it happens that for some reason models need to be persisted with save(validate: false), then some filter that was suppose to be always run on assigned fields does not get run. Sure, having invalid data is usually something you want to avoid, but there would be no need for such option if it wasn't used. Another problem with it is that every time you ask from a model is it valid these modifications also take place. The fact that simply asking if a model is valid may result in the model getting modified is just unexpected, perhaps even unwanted. There for if I'd have to choose a hook I'd go for before_save hook. However, that won't do it for me since we provide preview views for our models and that would break the URIs in the preview view since the hook would never get called. There for, I decided it's best to separate the concept in to a module or concern and provide a nice way for one to apply a "monkey patch" ensuring that changing the fields value always runs through a filter that adds a default protocol if it is missing.

The module

#app/models/helpers/uri_field.rb
module Helpers::URIField
  def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
    alias_method "original_#{field}=", "#{field}="
    define_method "#{field}=" do |new_uri|
      if "#{field}_changed?"
        if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
          new_uri = "#{default_protocol}://#{new_uri}"
        end
        self.send("original_#{field}=", new_uri)
      end
    end
  end
end

In your model

extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"

As a concern

If for some reason, you'd rather use the Rails Concern pattern it is easy to convert the above module to a concern module (it is used in an exactly similar way, except you use include Concerns::URIField:

#app/models/concerns/uri_field.rb
module Concerns::URIField
  extend ActiveSupport::Concern

  included do
    def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
      alias_method "original_#{field}=", "#{field}="
      define_method "#{field}=" do |new_uri|
        if "#{field}_changed?"
          if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
            new_uri = "#{default_protocol}://#{new_uri}"
          end
          self.send("original_#{field}=", new_uri)
        end
      end
    end
  end
end

P.S. The above approaches were tested with Rails 3 and Mongoid 2.
P.P.S If you find this method redefinition and aliasing too magical you could opt not to override the method, but rather use the virtual field pattern, much like password (virtual, mass assignable) and encrypted_password (gets persisted, non mass assignable) and use a sanitize_url (virtual, mass assignable) and url (gets persisted, non mass assignable).

Upvotes: 5

Muntasim
Muntasim

Reputation: 6786

The accepted answer is quite okay. But if the field (url) is optional, it may raise an error such as undefined method + for nil class. The following should resolve that:

def smart_add_url_protocol
  if self.url && !url_protocol_present?
    self.url = "http://#{self.url}"
  end
end

def url_protocol_present?
  self.url[/\Ahttp:\/\//] || self.url[/\Ahttps:\/\//]
end

Upvotes: 5

Michael
Michael

Reputation: 784

Based on mu's answer, here's the code I'm using in my model. This runs when :link is saved without the need for model filters. Super is required to call the default save method.

def link=(_link)
    u=URI.parse(_link)

    if (!u.scheme)
        link = "http://" + _link
    else
        link = _link
    end
    super(link)
end

Upvotes: 4

Dave Newton
Dave Newton

Reputation: 160191

I wouldn't try to do that in the validation, since it's not really part of the validation.

Have the validation optionally check for it; if they screw it up it'll be a validation error, which is good.

Consider using a callback (after_create, after_validation, whatever) to prepend a protocol if there isn't one there already.

(I voted up the other answers; I think they're both better than mine. But here's another option :)

Upvotes: 0

Related Questions