mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
The redirect routing method now allows for a hash of options which only changes the relevant parts of the url, or an object which responds to call can be supplied so common redirect rules can be easily reused. This commit includes a change where url generation from parts has been moved to AD::Http::URL as a class method.
This commit is contained in:
parent
2c08ee97c7
commit
0bda6f1ec6
7 changed files with 509 additions and 347 deletions
|
@ -24,6 +24,58 @@ module ActionDispatch
|
|||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
|
||||
def self.url_for(options = {})
|
||||
unless options[:host].present? || options[:only_path].present?
|
||||
raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
|
||||
end
|
||||
|
||||
rewritten_url = ""
|
||||
|
||||
unless options[:only_path]
|
||||
rewritten_url << (options[:protocol] || "http")
|
||||
rewritten_url << "://" unless rewritten_url.match("://")
|
||||
rewritten_url << rewrite_authentication(options)
|
||||
rewritten_url << host_or_subdomain_and_domain(options)
|
||||
rewritten_url << ":#{options.delete(:port)}" if options[:port]
|
||||
end
|
||||
|
||||
path = options.delete(:path) || ''
|
||||
|
||||
params = options[:params] || {}
|
||||
params.reject! {|k,v| !v }
|
||||
|
||||
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
|
||||
rewritten_url << "?#{params.to_query}" unless params.empty?
|
||||
rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
|
||||
rewritten_url
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def rewrite_authentication(options)
|
||||
if options[:user] && options[:password]
|
||||
"#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def host_or_subdomain_and_domain(options)
|
||||
return options[:host] unless options[:subdomain] || options[:domain]
|
||||
|
||||
tld_length = options[:tld_length] || @@tld_length
|
||||
|
||||
host = ""
|
||||
host << (options[:subdomain] || extract_subdomain(options[:host], tld_length))
|
||||
host << "."
|
||||
host << (options[:domain] || extract_domain(options[:host], tld_length))
|
||||
host
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Returns the complete URL used for this request.
|
||||
def url
|
||||
|
|
|
@ -2,6 +2,7 @@ require 'erb'
|
|||
require 'active_support/core_ext/hash/except'
|
||||
require 'active_support/core_ext/object/blank'
|
||||
require 'active_support/inflector'
|
||||
require 'action_dispatch/routing/redirection'
|
||||
|
||||
module ActionDispatch
|
||||
module Routing
|
||||
|
@ -383,39 +384,6 @@ module ActionDispatch
|
|||
map_method(:delete, *args, &block)
|
||||
end
|
||||
|
||||
# Redirect any path to another path:
|
||||
#
|
||||
# match "/stories" => redirect("/posts")
|
||||
def redirect(*args)
|
||||
options = args.last.is_a?(Hash) ? args.pop : {}
|
||||
|
||||
path = args.shift || Proc.new
|
||||
path_proc = path.is_a?(Proc) ? path : proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) }
|
||||
status = options[:status] || 301
|
||||
|
||||
lambda do |env|
|
||||
req = Request.new(env)
|
||||
|
||||
params = [req.symbolized_path_parameters]
|
||||
params << req if path_proc.arity > 1
|
||||
|
||||
uri = URI.parse(path_proc.call(*params))
|
||||
uri.scheme ||= req.scheme
|
||||
uri.host ||= req.host
|
||||
uri.port ||= req.port unless req.standard_port?
|
||||
|
||||
body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
|
||||
|
||||
headers = {
|
||||
'Location' => uri.to_s,
|
||||
'Content-Type' => 'text/html',
|
||||
'Content-Length' => body.length.to_s
|
||||
}
|
||||
|
||||
[ status, headers, [body] ]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def map_method(method, *args, &block)
|
||||
options = args.extract_options!
|
||||
|
@ -638,7 +606,7 @@ module ActionDispatch
|
|||
:shallow_path => path, :shallow_prefix => path }.merge!(options)
|
||||
scope(options) { yield }
|
||||
end
|
||||
|
||||
|
||||
# === Parameter Restriction
|
||||
# Allows you to constrain the nested routes based on a set of rules.
|
||||
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
|
||||
|
@ -649,7 +617,7 @@ module ActionDispatch
|
|||
#
|
||||
# Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
|
||||
# The +id+ parameter must match the constraint passed in for this example.
|
||||
#
|
||||
#
|
||||
# You may use this to also resrict other parameters:
|
||||
#
|
||||
# resources :posts do
|
||||
|
@ -1340,6 +1308,7 @@ module ActionDispatch
|
|||
|
||||
include Base
|
||||
include HttpHelpers
|
||||
include Redirection
|
||||
include Scoping
|
||||
include Resources
|
||||
include Shorthand
|
||||
|
|
81
actionpack/lib/action_dispatch/routing/redirection.rb
Normal file
81
actionpack/lib/action_dispatch/routing/redirection.rb
Normal file
|
@ -0,0 +1,81 @@
|
|||
require 'action_dispatch/http/request'
|
||||
|
||||
module ActionDispatch
|
||||
module Routing
|
||||
module Redirection
|
||||
|
||||
# Redirect any path to another path:
|
||||
#
|
||||
# match "/stories" => redirect("/posts")
|
||||
def redirect(*args, &block)
|
||||
options = args.last.is_a?(Hash) ? args.pop : {}
|
||||
status = options.delete(:status) || 301
|
||||
|
||||
path = args.shift
|
||||
|
||||
path_proc = if path.is_a?(String)
|
||||
proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) }
|
||||
elsif options.any?
|
||||
options_proc(options)
|
||||
elsif path.respond_to?(:call)
|
||||
proc { |params, request| path.call(params, request) }
|
||||
elsif block
|
||||
block
|
||||
else
|
||||
raise ArgumentError, "redirection argument not supported"
|
||||
end
|
||||
|
||||
redirection_proc(status, path_proc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def options_proc(options)
|
||||
proc do |params, request|
|
||||
path = if options[:path].nil?
|
||||
request.path
|
||||
elsif params.empty? || !options[:path].match(/%\{\w*\}/)
|
||||
options.delete(:path)
|
||||
else
|
||||
(options.delete(:path) % params)
|
||||
end
|
||||
|
||||
default_options = {
|
||||
:protocol => request.protocol,
|
||||
:host => request.host,
|
||||
:port => request.optional_port,
|
||||
:path => path,
|
||||
:params => request.query_parameters
|
||||
}
|
||||
|
||||
ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options))
|
||||
end
|
||||
end
|
||||
|
||||
def redirection_proc(status, path_proc)
|
||||
lambda do |env|
|
||||
req = Request.new(env)
|
||||
|
||||
params = [req.symbolized_path_parameters]
|
||||
params << req if path_proc.arity > 1
|
||||
|
||||
uri = URI.parse(path_proc.call(*params))
|
||||
uri.scheme ||= req.scheme
|
||||
uri.host ||= req.host
|
||||
uri.port ||= req.port unless req.standard_port?
|
||||
|
||||
body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
|
||||
|
||||
headers = {
|
||||
'Location' => uri.to_s,
|
||||
'Content-Type' => 'text/html',
|
||||
'Content-Length' => body.length.to_s
|
||||
}
|
||||
|
||||
[ status, headers, [body] ]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -442,12 +442,9 @@ module ActionDispatch
|
|||
|
||||
raise_routing_error unless path
|
||||
|
||||
params.reject! {|k,v| !v }
|
||||
|
||||
return [path, params.keys] if @extras
|
||||
|
||||
path << "?#{params.to_query}" unless params.empty?
|
||||
path
|
||||
[path, params]
|
||||
rescue Rack::Mount::RoutingError
|
||||
raise_routing_error
|
||||
end
|
||||
|
@ -486,7 +483,7 @@ module ActionDispatch
|
|||
end
|
||||
|
||||
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
|
||||
:trailing_slash, :script_name, :anchor, :params, :only_path ]
|
||||
:trailing_slash, :anchor, :params, :only_path, :script_name]
|
||||
|
||||
def _generate_prefix(options = {})
|
||||
nil
|
||||
|
@ -498,29 +495,24 @@ module ActionDispatch
|
|||
|
||||
handle_positional_args(options)
|
||||
|
||||
rewritten_url = ""
|
||||
user, password = extract_authentication(options)
|
||||
path_segments = options.delete(:_path_segments)
|
||||
script_name = options.delete(:script_name)
|
||||
|
||||
path_segments = options.delete(:_path_segments)
|
||||
unless options[:only_path]
|
||||
rewritten_url << (options[:protocol] || "http")
|
||||
rewritten_url << "://" unless rewritten_url.match("://")
|
||||
rewritten_url << rewrite_authentication(options)
|
||||
rewritten_url << host_from_options(options)
|
||||
rewritten_url << ":#{options.delete(:port)}" if options[:port]
|
||||
end
|
||||
|
||||
script_name = options.delete(:script_name)
|
||||
path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
|
||||
|
||||
path_options = options.except(*RESERVED_OPTIONS)
|
||||
path_options = yield(path_options) if block_given?
|
||||
path << generate(path_options, path_segments || {})
|
||||
|
||||
# ROUTES TODO: This can be called directly, so script_name should probably be set in the routes
|
||||
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
|
||||
rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
|
||||
path_addition, params = generate(path_options, path_segments || {})
|
||||
path << path_addition
|
||||
|
||||
rewritten_url
|
||||
ActionDispatch::Http::URL.url_for(options.merge({
|
||||
:path => path,
|
||||
:params => params,
|
||||
:user => user,
|
||||
:password => password
|
||||
}))
|
||||
end
|
||||
|
||||
def call(env)
|
||||
|
@ -561,23 +553,12 @@ module ActionDispatch
|
|||
|
||||
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]"
|
||||
def extract_authentication(options)
|
||||
if options[:user] && options[:password]
|
||||
[options.delete(:user), options.delete(:password)]
|
||||
else
|
||||
nil
|
||||
end
|
||||
computed_host
|
||||
end
|
||||
|
||||
def subdomain_and_domain(options)
|
||||
return nil unless options[:subdomain] || options[:domain]
|
||||
tld_length = options[:tld_length] || ActionDispatch::Http::URL.tld_length
|
||||
|
||||
host = ""
|
||||
host << (options[:subdomain] || ActionDispatch::Http::URL.extract_subdomain(options[:host], tld_length))
|
||||
host << "."
|
||||
host << (options[:domain] || ActionDispatch::Http::URL.extract_domain(options[:host], tld_length))
|
||||
host
|
||||
end
|
||||
|
||||
def handle_positional_args(options)
|
||||
|
@ -590,13 +571,6 @@ module ActionDispatch
|
|||
options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
|
||||
end
|
||||
|
||||
def rewrite_authentication(options)
|
||||
if options[:user] && options[:password]
|
||||
"#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,31 @@
|
|||
require 'abstract_unit'
|
||||
|
||||
class RequestTest < ActiveSupport::TestCase
|
||||
|
||||
def url_for(options = {})
|
||||
options.reverse_merge!(:host => 'www.example.com')
|
||||
ActionDispatch::Http::URL.url_for(options)
|
||||
end
|
||||
|
||||
test "url_for class method" do
|
||||
e = assert_raise(ArgumentError) { url_for(:host => nil) }
|
||||
assert_match(/Please provide the :host parameter/, e.message)
|
||||
|
||||
assert_equal '/books', url_for(:only_path => true, :path => '/books')
|
||||
|
||||
assert_equal 'http://www.example.com', url_for
|
||||
assert_equal 'http://api.example.com', url_for(:subdomain => 'api')
|
||||
assert_equal 'http://www.ror.com', url_for(:domain => 'ror.com')
|
||||
assert_equal 'http://api.ror.co.uk', url_for(:host => 'www.ror.co.uk', :subdomain => 'api', :tld_length => 2)
|
||||
assert_equal 'http://www.example.com:8080', url_for(:port => 8080)
|
||||
assert_equal 'https://www.example.com', url_for(:protocol => 'https')
|
||||
assert_equal 'http://www.example.com/docs', url_for(:path => '/docs')
|
||||
assert_equal 'http://www.example.com#signup', url_for(:anchor => 'signup')
|
||||
assert_equal 'http://www.example.com/', url_for(:trailing_slash => true)
|
||||
assert_equal 'http://dhh:supersecret@www.example.com', url_for(:user => 'dhh', :password => 'supersecret')
|
||||
assert_equal 'http://www.example.com?search=books', url_for(:params => { :search => 'books' })
|
||||
end
|
||||
|
||||
test "remote ip" do
|
||||
request = stub_request 'REMOTE_ADDR' => '1.2.3.4'
|
||||
assert_equal '1.2.3.4', request.remote_ip
|
||||
|
|
|
@ -13,6 +13,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
|||
end
|
||||
end
|
||||
|
||||
class YoutubeFavoritesRedirector
|
||||
def self.call(params, request)
|
||||
"http://www.youtube.com/watch?v=#{params[:youtube_id]}"
|
||||
end
|
||||
end
|
||||
|
||||
stub_controllers do |routes|
|
||||
Routes = routes
|
||||
Routes.draw do
|
||||
|
@ -54,6 +60,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
|||
match 'account/login', :to => redirect("/login")
|
||||
match 'secure', :to => redirect("/secure/login")
|
||||
|
||||
match 'mobile', :to => redirect(:subdomain => 'mobile')
|
||||
match 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '')
|
||||
match 'new_documentation', :to => redirect(:path => '/documentation/new')
|
||||
match 'super_new_documentation', :to => redirect(:host => 'super-docs.com')
|
||||
|
||||
match 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector)
|
||||
|
||||
constraints(lambda { |req| true }) do
|
||||
match 'account/overview'
|
||||
end
|
||||
|
@ -667,6 +680,41 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
|
|||
end
|
||||
end
|
||||
|
||||
def test_redirect_hash_with_subdomain
|
||||
with_test_routes do
|
||||
get '/mobile'
|
||||
verify_redirect 'http://mobile.example.com/mobile'
|
||||
end
|
||||
end
|
||||
|
||||
def test_redirect_hash_with_domain_and_path
|
||||
with_test_routes do
|
||||
get '/documentation'
|
||||
verify_redirect 'http://www.example-documentation.com'
|
||||
end
|
||||
end
|
||||
|
||||
def test_redirect_hash_with_path
|
||||
with_test_routes do
|
||||
get '/new_documentation'
|
||||
verify_redirect 'http://www.example.com/documentation/new'
|
||||
end
|
||||
end
|
||||
|
||||
def test_redirect_hash_with_host
|
||||
with_test_routes do
|
||||
get '/super_new_documentation?section=top'
|
||||
verify_redirect 'http://super-docs.com/super_new_documentation?section=top'
|
||||
end
|
||||
end
|
||||
|
||||
def test_redirect_class
|
||||
with_test_routes do
|
||||
get '/youtube_favorites/oHg5SJYRHA0/rick-rolld'
|
||||
verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0'
|
||||
end
|
||||
end
|
||||
|
||||
def test_openid
|
||||
with_test_routes do
|
||||
get '/openid/login'
|
||||
|
|
Loading…
Reference in a new issue