提交 78afe68a 编写于 作者: J José Valim

Merge remote branch 'joshk/redirect_routing'

Conflicts:
	actionpack/CHANGELOG
	actionpack/lib/action_controller/metal/mime_responds.rb
Signed-off-by: NJosé Valim <jose.valim@gmail.com>
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
* url_for and named url helpers now accept :subdomain and :domain as options [Josh Kalderimis] * url_for and named url helpers now accept :subdomain and :domain as options [Josh Kalderimis]
* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused (check the documentation for examples). [Josh Kalderimis]
* Added config.action_controller.include_all_helpers. By default 'helper :all' is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and helper corresponding to controller (like foo_helper for foo_controller). [Piotr Sarnacki] * Added config.action_controller.include_all_helpers. By default 'helper :all' is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and helper corresponding to controller (like foo_helper for foo_controller). [Piotr Sarnacki]
* Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options: * Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options:
......
...@@ -24,6 +24,58 @@ def self.named_host?(host) ...@@ -24,6 +24,58 @@ def self.named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end 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. # Returns the complete URL used for this request.
def url def url
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/blank'
require 'active_support/inflector' require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
module ActionDispatch module ActionDispatch
module Routing module Routing
...@@ -383,39 +384,6 @@ def delete(*args, &block) ...@@ -383,39 +384,6 @@ def delete(*args, &block)
map_method(:delete, *args, &block) map_method(:delete, *args, &block)
end 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 private
def map_method(method, *args, &block) def map_method(method, *args, &block)
options = args.extract_options! options = args.extract_options!
...@@ -636,7 +604,7 @@ def namespace(path, options = {}) ...@@ -636,7 +604,7 @@ def namespace(path, options = {})
:shallow_path => path, :shallow_prefix => path }.merge!(options) :shallow_path => path, :shallow_prefix => path }.merge!(options)
scope(options) { yield } scope(options) { yield }
end end
# === Parameter Restriction # === Parameter Restriction
# Allows you to constrain the nested routes based on a set of rules. # 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: # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
...@@ -647,7 +615,7 @@ def namespace(path, options = {}) ...@@ -647,7 +615,7 @@ def namespace(path, options = {})
# #
# Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. # 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. # The +id+ parameter must match the constraint passed in for this example.
# #
# You may use this to also resrict other parameters: # You may use this to also resrict other parameters:
# #
# resources :posts do # resources :posts do
...@@ -1369,6 +1337,7 @@ def match(*args) ...@@ -1369,6 +1337,7 @@ def match(*args)
include Base include Base
include HttpHelpers include HttpHelpers
include Redirection
include Scoping include Scoping
include Resources include Resources
include Shorthand include Shorthand
......
require 'action_dispatch/http/request'
module ActionDispatch
module Routing
module Redirection
# Redirect any path to another path:
#
# match "/stories" => redirect("/posts")
#
# You can also use interpolation in the supplied redirect argument:
#
# match 'docs/:article', :to => redirect('/wiki/%{article}')
#
# Alternatively you can use one of the other syntaxes:
#
# The block version of redirect allows for the easy encapsulation of any logic associated with
# the redirect in question. Either the params and request are supplied as arguments, or just
# params, depending of how many arguments your block accepts. A string is required as a
# return value.
#
# match 'jokes/:number', :to => redirect do |params, request|
# path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp")
# "http://#{request.host_with_port}/#{path}"
# end
#
# The options version of redirect allows you to supply only the parts of the url which need
# to change, it also supports interpolation of the path similar to the first example.
#
# match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
# match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}')
#
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
# common redirect routes. The call method must accept two arguments, params and request, and return
# a string.
#
# match 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
#
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
\ No newline at end of file
...@@ -442,12 +442,9 @@ def generate ...@@ -442,12 +442,9 @@ def generate
raise_routing_error unless path raise_routing_error unless path
params.reject! {|k,v| !v }
return [path, params.keys] if @extras return [path, params.keys] if @extras
path << "?#{params.to_query}" unless params.empty? [path, params]
path
rescue Rack::Mount::RoutingError rescue Rack::Mount::RoutingError
raise_routing_error raise_routing_error
end end
...@@ -486,7 +483,7 @@ def generate(options, recall = {}, extras = false) ...@@ -486,7 +483,7 @@ def generate(options, recall = {}, extras = false)
end end
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, 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 = {}) def _generate_prefix(options = {})
nil nil
...@@ -498,29 +495,24 @@ def url_for(options) ...@@ -498,29 +495,24 @@ def url_for(options)
handle_positional_args(options) handle_positional_args(options)
rewritten_url = "" user, password = extract_authentication(options)
path_segments = options.delete(:_path_segments)
path_segments = options.delete(:_path_segments) script_name = options.delete(:script_name)
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 = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
path_options = options.except(*RESERVED_OPTIONS) path_options = options.except(*RESERVED_OPTIONS)
path_options = yield(path_options) if block_given? 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 path_addition, params = generate(path_options, path_segments || {})
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) path << path_addition
rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
rewritten_url ActionDispatch::Http::URL.url_for(options.merge({
:path => path,
:params => params,
:user => user,
:password => password
}))
end end
def call(env) def call(env)
...@@ -561,23 +553,12 @@ def recognize_path(path, environment = {}) ...@@ -561,23 +553,12 @@ def recognize_path(path, environment = {})
private private
def host_from_options(options) def extract_authentication(options)
computed_host = subdomain_and_domain(options) || options[:host] if options[:user] && options[:password]
unless computed_host [options.delete(:user), options.delete(:password)]
raise ArgumentError, "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" else
nil
end 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 end
def handle_positional_args(options) def handle_positional_args(options)
...@@ -590,13 +571,6 @@ def handle_positional_args(options) ...@@ -590,13 +571,6 @@ def handle_positional_args(options)
options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }]) options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
end 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 end
end end
require 'abstract_unit' require 'abstract_unit'
class RequestTest < ActiveSupport::TestCase 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 test "remote ip" do
request = stub_request 'REMOTE_ADDR' => '1.2.3.4' request = stub_request 'REMOTE_ADDR' => '1.2.3.4'
assert_equal '1.2.3.4', request.remote_ip assert_equal '1.2.3.4', request.remote_ip
......
...@@ -13,6 +13,12 @@ def self.matches?(request) ...@@ -13,6 +13,12 @@ def self.matches?(request)
end end
end end
class YoutubeFavoritesRedirector
def self.call(params, request)
"http://www.youtube.com/watch?v=#{params[:youtube_id]}"
end
end
stub_controllers do |routes| stub_controllers do |routes|
Routes = routes Routes = routes
Routes.draw do Routes.draw do
...@@ -54,6 +60,16 @@ def self.matches?(request) ...@@ -54,6 +60,16 @@ def self.matches?(request)
match 'account/login', :to => redirect("/login") match 'account/login', :to => redirect("/login")
match 'secure', :to => redirect("/secure/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 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
match 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}')
match 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector)
constraints(lambda { |req| true }) do constraints(lambda { |req| true }) do
match 'account/overview' match 'account/overview'
end end
...@@ -667,6 +683,55 @@ def test_redirect_proc_with_request ...@@ -667,6 +683,55 @@ def test_redirect_proc_with_request
end end
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_hash_path_substitution
with_test_routes do
get '/stores/iernest'
verify_redirect 'http://stores.example.com/iernest'
end
end
def test_redirect_hash_path_substitution_with_catch_all
with_test_routes do
get '/stores/iernest/products'
verify_redirect 'http://stores.example.com/iernest/products'
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 def test_openid
with_test_routes do with_test_routes do
get '/openid/login' get '/openid/login'
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册