aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/http/url.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/http/url.rb')
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb274
1 files changed, 231 insertions, 43 deletions
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index bced7d84c0..001b14ec97 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,90 +1,187 @@
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/hash/slice'
+
module ActionDispatch
module Http
module URL
- IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+ IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+ HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
+ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
self.tld_length = 1
class << self
- def extract_domain(host, tld_length = @@tld_length)
- host.split('.').last(1 + tld_length).join('.') if named_host?(host)
+ # Returns the domain part of a host given the domain level.
+ #
+ # # Top-level domain example
+ # extract_domain('www.example.com', 1) # => "example.com"
+ # # Second-level domain example
+ # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
+ def extract_domain(host, tld_length)
+ extract_domain_from(host, tld_length) if named_host?(host)
end
- def extract_subdomains(host, tld_length = @@tld_length)
+ # Returns the subdomains of a host as an Array given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomains('www.example.com', 1) # => ["www"]
+ # # Second-level domain example
+ # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
+ def extract_subdomains(host, tld_length)
if named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
+ extract_subdomains_from(host, tld_length)
else
[]
end
end
- def extract_subdomain(host, tld_length = @@tld_length)
+ # Returns the subdomains of a host as a String given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomain('www.example.com', 1) # => "www"
+ # # Second-level domain example
+ # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
+ def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join('.')
end
- def url_for(options = {})
- path = options.delete(:script_name).to_s.chomp("/")
- path << options.delete(:path).to_s
+ def url_for(options)
+ if options[:only_path]
+ path_for options
+ else
+ full_url_for options
+ end
+ end
- params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params)
- params.reject! { |_,v| v.to_param.nil? }
+ def full_url_for(options)
+ host = options[:host]
+ protocol = options[:protocol]
+ port = options[:port]
- result = build_host_url(options)
- result << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
- result << "?#{params.to_query}" unless params.empty?
- result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- result
+ unless host
+ raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ end
+
+ build_host_url(host, port, protocol, options, path_for(options))
+ end
+
+ def path_for(options)
+ path = options[:script_name].to_s.chomp("/")
+ path << options[:path] if options.key?(:path)
+
+ add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
+
+ path
end
private
- def build_host_url(options)
- if options[:host].blank? && options[:only_path].blank?
- raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_,v| v.to_param.nil? }
+ path << "?#{params.to_query}" unless params.empty?
+ end
+
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
+ end
+
+ def extract_domain_from(host, tld_length)
+ host.split('.').last(1 + tld_length).join('.')
+ end
+
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split('.')
+ parts[0..-(tld_length + 2)]
+ end
+
+ def add_trailing_slash(path)
+ # includes querysting
+ if path.include?('?')
+ path.sub!(/\?/, '/\&')
+ # does not have a .format
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
+ end
+
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
end
- result = ""
-
- unless options[:only_path]
- unless options[:protocol] == false
- result << (options[:protocol] || "http")
- result << ":" unless result.match(%r{:|//})
- end
- result << "//" unless result.match("//")
- result << rewrite_authentication(options)
- result << host_or_subdomain_and_domain(options)
- result << ":#{options.delete(:port)}" if options[:port]
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
+
+ result = protocol.dup
+
+ if options[:user] && options[:password]
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
end
- result
+
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
+
+ result.concat path
end
def named_host?(host)
- host && IP_HOST_REGEXP !~ host
+ IP_HOST_REGEXP !~ host
end
- def rewrite_authentication(options)
- if options[:user] && options[:password]
- "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ def normalize_protocol(protocol)
+ case protocol
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
else
- ""
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
end
end
- def host_or_subdomain_and_domain(options)
- return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?)
+ def normalize_host(_host, options)
+ return _host unless named_host?(_host)
tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
host = ""
- unless options[:subdomain] == false
- host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param
- host << "."
+ if subdomain == true
+ return _host if domain.nil?
+
+ host << extract_subdomains_from(_host, tld_length).join('.')
+ elsif subdomain
+ host << subdomain.to_param
end
- host << (options[:domain] || extract_domain(options[:host], tld_length))
+ host << "." unless host.empty?
+ host << (domain || extract_domain_from(_host, tld_length))
host
end
+
+ def normalize_port(port, protocol)
+ return unless port
+
+ case protocol
+ when "//" then yield port
+ when "https://"
+ yield port unless port.to_i == 443
+ else
+ yield port unless port.to_i == 80
+ end
+ end
end
def initialize(env)
@@ -94,16 +191,43 @@ module ActionDispatch
end
# Returns the complete URL used for this request.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.url # => "http://example.com"
def url
protocol + host_with_port + fullpath
end
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.protocol # => "http://"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req.protocol # => "https://"
def protocol
@protocol ||= ssl? ? 'https://' : 'http://'
end
# Returns the \host for this request, such as "example.com".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.raw_host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
@@ -113,17 +237,44 @@ module ActionDispatch
end
# Returns the host for this request, such as example.com.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host # => "example.com"
def host
raw_host_with_port.sub(/:\d+$/, '')
end
# Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host_with_port # => "example.com:8080"
def host_with_port
"#{host}#{port_string}"
end
# Returns the port number of this request as an integer.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.port # => 80
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port # => 8080
def port
@port ||= begin
if raw_host_with_port =~ /:(\d+)$/
@@ -135,6 +286,13 @@ module ActionDispatch
end
# Returns the standard \port number for this request's protocol.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port # => 80
def standard_port
case protocol
when 'https://' then 443
@@ -143,18 +301,48 @@ module ActionDispatch
end
# Returns whether this request is using the standard port
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.standard_port? # => true
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port? # => false
def standard_port?
port == standard_port
end
# Returns a number \port suffix like 8080 if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.optional_port # => nil
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.optional_port # => 8080
def optional_port
standard_port? ? nil : port
end
# Returns a string \port suffix, including colon, like ":8080" if the \port
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.port_string # => ""
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port_string # => ":8080"
def port_string
standard_port? ? '' : ":#{port}"
end