Skip to content

Commit

Permalink
:subdomain, :domain and :tld_length options can now be used in url_fo…
Browse files Browse the repository at this point in the history
…r, allowing for easy manipulation of the host during link generation.

Signed-off-by: José Valim <jose.valim@gmail.com>
  • Loading branch information
joshk authored and josevalim committed Nov 23, 2010
1 parent 9938a3f commit 2fe43b6
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 31 deletions.
4 changes: 3 additions & 1 deletion actionpack/lib/action_controller/metal/url_for.rb
Expand Up @@ -6,7 +6,8 @@ module UrlFor


def url_options def url_options
@_url_options ||= super.reverse_merge( @_url_options ||= super.reverse_merge(
:host => request.host_with_port, :host => request.host,
:port => request.optional_port,
:protocol => request.protocol, :protocol => request.protocol,
:_path_segments => request.symbolized_path_parameters :_path_segments => request.symbolized_path_parameters
).freeze ).freeze
Expand All @@ -20,5 +21,6 @@ def url_options
@_url_options @_url_options
end end
end end

end end
end end
59 changes: 39 additions & 20 deletions actionpack/lib/action_dispatch/http/url.rb
Expand Up @@ -4,6 +4,27 @@ module URL
mattr_accessor :tld_length mattr_accessor :tld_length
self.tld_length = 1 self.tld_length = 1


def self.extract_domain(host, tld_length = @@tld_length)
return nil unless named_host?(host)

host.split('.').last(1 + tld_length).join('.')
end

def self.extract_subdomains(host, tld_length = @@tld_length)
return [] unless named_host?(host)
parts = host.split('.')
parts[0..-(tld_length+2)]
end

def self.extract_subdomain(host, tld_length = @@tld_length)
extract_subdomains(host, tld_length).join('.')
end

def self.named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end


# Returns the complete URL used for this request. # Returns the complete URL used for this request.
def url def url
protocol + host_with_port + fullpath protocol + host_with_port + fullpath
Expand Down Expand Up @@ -31,15 +52,18 @@ def host
# Returns a \host:\port string for this request, such as "example.com" or # Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080". # "example.com:8080".
def host_with_port def host_with_port
"#{host}#{port_string}" opt_port = optional_port ? ":#{optional_port}" : nil
"#{host}#{opt_port}"
end end


# Returns the port number of this request as an integer. # Returns the port number of this request as an integer.
def port def port
if raw_host_with_port =~ /:(\d+)$/ @port ||= begin
$1.to_i if raw_host_with_port =~ /:(\d+)$/
else $1.to_i
standard_port else
standard_port
end
end end
end end


Expand All @@ -56,10 +80,10 @@ def standard_port?
port == standard_port port == standard_port
end end


# Returns a \port suffix like ":8080" if the \port number of this request # Returns a \port suffix like "8080" if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443. # is not the default HTTP \port 80 or HTTPS \port 443.
def port_string def optional_port
port == standard_port ? '' : ":#{port}" standard_port? ? nil : port
end end


def server_port def server_port
Expand All @@ -69,30 +93,25 @@ def server_port
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = @@tld_length) def domain(tld_length = @@tld_length)
return nil unless named_host?(host) ActionDispatch::Http::URL.extract_domain(host, tld_length)

host.split('.').last(1 + tld_length).join('.')
end end


# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
# in "www.rubyonrails.co.uk". # in "www.rubyonrails.co.uk".
def subdomains(tld_length = @@tld_length) def subdomains(tld_length = @@tld_length)
return [] unless named_host?(host) ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
parts = host.split('.')
parts[0..-(tld_length+2)]
end end


# Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>"www.rubyonrails"</tt>
# in "www.rubyonrails.co.uk".
def subdomain(tld_length = @@tld_length) def subdomain(tld_length = @@tld_length)
subdomains(tld_length).join('.') subdomains(tld_length)
end end


private

def named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
end end
end end
end end
38 changes: 32 additions & 6 deletions actionpack/lib/action_dispatch/routing/route_set.rb
Expand Up @@ -485,7 +485,8 @@ def generate(options, recall = {}, extras = false)
Generator.new(options, recall, self, extras).generate Generator.new(options, recall, self, extras).generate
end end


RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :script_name] RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :script_name, :anchor, :params, :only_path ]


def _generate_prefix(options = {}) def _generate_prefix(options = {})
nil nil
Expand All @@ -504,11 +505,8 @@ def url_for(options)
rewritten_url << (options[:protocol] || "http") rewritten_url << (options[:protocol] || "http")
rewritten_url << "://" unless rewritten_url.match("://") rewritten_url << "://" unless rewritten_url.match("://")
rewritten_url << rewrite_authentication(options) rewritten_url << rewrite_authentication(options)

rewritten_url << host_from_options(options)
raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] rewritten_url << ":#{options.delete(:port)}" if options[:port]

rewritten_url << options[:host]
rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
end end


script_name = options.delete(:script_name) script_name = options.delete(:script_name)
Expand Down Expand Up @@ -562,6 +560,34 @@ def recognize_path(path, environment = {})
end end


private private

def host_from_options(options)
computed_host = subdomain_and_domain(options) || options[:host]
unless computed_host
raise ArgumentError, "Missing host to link to! Please provide :host parameter or set default_url_options[:host]"
end
computed_host
end

def subdomain_and_domain(options)
tld_length = options[:tld_length] || ActionDispatch::Http::URL.tld_length

current_domain = ActionDispatch::Http::URL.extract_domain(options[:host], tld_length)
current_subdomain = ActionDispatch::Http::URL.extract_subdomain(options[:host], tld_length)

domain_parts = if options[:subdomain] && options[:domain]
[options[:subdomain], options[:domain]]
elsif options[:subdomain]
[options[:subdomain], current_domain]
elsif options[:domain]
[current_subdomain, options[:domain]]
else
nil
end

domain_parts ? domain_parts.join('.') : nil
end

def handle_positional_args(options) def handle_positional_args(options)
return unless args = options.delete(:_positional_args) return unless args = options.delete(:_positional_args)


Expand Down
6 changes: 6 additions & 0 deletions actionpack/lib/action_dispatch/routing/url_for.rb
Expand Up @@ -115,6 +115,12 @@ def url_options
# * <tt>:host</tt> - Specifies the host the link should be targeted at. # * <tt>:host</tt> - Specifies the host the link should be targeted at.
# If <tt>:only_path</tt> is false, this option must be # If <tt>:only_path</tt> is false, this option must be
# provided either explicitly, or via +default_url_options+. # provided either explicitly, or via +default_url_options+.
# * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+
# to split the domain from the host.
# * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+
# to split the subdomain from the host.
# * <tt>:tld_length</tt> - Optionally specify the tld length (only used if :subdomain
# or :domain are supplied).
# * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path. # * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
Expand Down
23 changes: 22 additions & 1 deletion actionpack/test/controller/url_for_test.rb
Expand Up @@ -17,7 +17,7 @@ def add_host!
end end


def test_exception_is_thrown_without_host def test_exception_is_thrown_without_host
assert_raise RuntimeError do assert_raise ArgumentError do
W.new.url_for :controller => 'c', :action => 'a', :id => 'i' W.new.url_for :controller => 'c', :action => 'a', :id => 'i'
end end
end end
Expand Down Expand Up @@ -60,6 +60,27 @@ def test_host_may_be_overridden
) )
end end


def test_subdomain_may_be_changed
add_host!
assert_equal('http://api.basecamphq.com/c/a/i',
W.new.url_for(:subdomain => 'api', :controller => 'c', :action => 'a', :id => 'i')
)
end

def test_domain_may_be_changed
add_host!
assert_equal('http://www.37signals.com/c/a/i',
W.new.url_for(:domain => '37signals.com', :controller => 'c', :action => 'a', :id => 'i')
)
end

def test_tld_length_may_be_changed
add_host!
assert_equal('http://mobile.www.basecamphq.com/c/a/i',
W.new.url_for(:subdomain => 'mobile', :tld_length => 2, :controller => 'c', :action => 'a', :id => 'i')
)
end

def test_port def test_port
add_host! add_host!
assert_equal('http://www.basecamphq.com:3000/c/a/i', assert_equal('http://www.basecamphq.com:3000/c/a/i',
Expand Down
6 changes: 3 additions & 3 deletions actionpack/test/dispatch/request_test.rb
Expand Up @@ -164,12 +164,12 @@ class RequestTest < ActiveSupport::TestCase
assert !request.standard_port? assert !request.standard_port?
end end


test "port string" do test "optional port" do
request = stub_request 'HTTP_HOST' => 'www.example.org:80' request = stub_request 'HTTP_HOST' => 'www.example.org:80'
assert_equal "", request.port_string assert_equal nil, request.optional_port


request = stub_request 'HTTP_HOST' => 'www.example.org:8080' request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
assert_equal ":8080", request.port_string assert_equal 8080, request.optional_port
end end


test "full path" do test "full path" do
Expand Down

3 comments on commit 2fe43b6

@duncanbeevers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

@jeffkreeftmeijer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work Josh! :D

@GICodeWarrior
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use "domain" as a query parameter throughout my application. Do I need to modify every route, url_for, link_to, and redirect_to to use something different? :-(

Please sign in to comment.