Skip to content

Commit 5460591

Browse files
committed
Make URL escaping more consistent
1. Escape '%' characters in URLs - only unescaped data should be passed to URL helpers 2. Add an `escape_segment` helper to `Router::Utils` that escapes '/' characters 3. Use `escape_segment` rather than `escape_fragment` in optimized URL generation 4. Use `escape_segment` rather than `escape_path` in URL generation For point 4 there are two exceptions. Firstly, when a route uses wildcard segments (e.g. *foo) then we use `escape_path` as the value may contain '/' characters. This means that wildcard routes can't be optimized. Secondly, if a `:controller` segment is used in the path then this uses `escape_path` as the controller may be namespaced. Fixes #14629, #14636 and #14070.
1 parent a617925 commit 5460591

8 files changed

Lines changed: 89 additions & 10 deletions

File tree

actionpack/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
* Make URL escaping more consistent:
2+
3+
1. Escape '%' characters in URLs - only unescaped data should be passed to URL helpers
4+
2. Add an `escape_segment` helper to `Router::Utils` that escapes '/' characters
5+
3. Use `escape_segment` rather than `escape_fragment` in optimized URL generation
6+
4. Use `escape_segment` rather than `escape_path` in URL generation
7+
8+
For point 4 there are two exceptions. Firstly, when a route uses wildcard segments
9+
(e.g. *foo) then we use `escape_path` as the value may contain '/' characters. This
10+
means that wildcard routes can't be optimized. Secondly, if a `:controller` segment
11+
is used in the path then this uses `escape_path` as the controller may be namespaced.
12+
13+
Fixes #14629, #14636 and #14070.
14+
15+
*Andrew White*, *Edho Arief*
16+
117
* Add alias `ActionDispatch::Http::UploadedFile#to_io` to
218
`ActionDispatch::Http::UploadedFile#tempfile`.
319

actionpack/lib/action_dispatch/journey/route.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ def required_defaults
101101
end
102102
end
103103

104+
def glob?
105+
!path.spec.grep(Nodes::Star).empty?
106+
end
107+
104108
def dispatcher?
105109
@dispatcher
106110
end

actionpack/lib/action_dispatch/journey/router/utils.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class UriEncoder # :nodoc:
3737
ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
3838

3939
FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
40+
SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
4041
PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
4142

4243
def escape_fragment(fragment)
@@ -47,6 +48,10 @@ def escape_path(path)
4748
escape(path, PATH)
4849
end
4950

51+
def escape_segment(segment)
52+
escape(segment, SEGMENT)
53+
end
54+
5055
def unescape_uri(uri)
5156
uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding)
5257
end
@@ -69,6 +74,10 @@ def self.escape_path(path)
6974
ENCODER.escape_path(path.to_s)
7075
end
7176

77+
def self.escape_segment(segment)
78+
ENCODER.escape_segment(segment.to_s)
79+
end
80+
7281
def self.escape_fragment(fragment)
7382
ENCODER.escape_fragment(fragment.to_s)
7483
end

actionpack/lib/action_dispatch/journey/visitors.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,26 @@ def initialize(options)
114114
end
115115

116116
private
117+
def escape_path(value)
118+
Router::Utils.escape_path(value)
119+
end
120+
121+
def escape_segment(value)
122+
Router::Utils.escape_segment(value)
123+
end
117124

118125
def visit(node, optional = false)
119126
case node.type
120127
when :LITERAL, :SLASH, :DOT
121128
node.left
122129
when :STAR
123-
visit(node.left)
130+
visit_STAR(node.left)
124131
when :GROUP
125132
visit(node.left, true)
126133
when :CAT
127134
visit_CAT(node, optional)
128135
when :SYMBOL
129-
visit_SYMBOL(node)
136+
visit_SYMBOL(node, node.to_sym)
130137
end
131138
end
132139

@@ -141,9 +148,15 @@ def visit_CAT(node, optional)
141148
end
142149
end
143150

144-
def visit_SYMBOL(node)
151+
def visit_STAR(node)
145152
if value = options[node.to_sym]
146-
Router::Utils.escape_path(value)
153+
escape_path(value)
154+
end
155+
end
156+
157+
def visit_SYMBOL(node, name)
158+
if value = options[name]
159+
name == :controller ? escape_path(value) : escape_segment(value)
147160
end
148161
end
149162
end

actionpack/lib/action_dispatch/routing/route_set.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def self.create(route, options)
155155
end
156156

157157
def self.optimize_helper?(route)
158-
route.requirements.except(:controller, :action).empty?
158+
!route.glob? && route.requirements.except(:controller, :action).empty?
159159
end
160160

161161
class OptimizedUrlHelper < UrlHelper # :nodoc:
@@ -194,7 +194,7 @@ def optimized_helper(args)
194194
end
195195

196196
def replace_segment(params, segment)
197-
Symbol === segment ? @klass.escape_fragment(params[segment]) : segment
197+
Symbol === segment ? @klass.escape_segment(params[segment]) : segment
198198
end
199199

200200
def optimize_routes_generation?(t)

actionpack/test/dispatch/routing_test.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3596,16 +3596,16 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
35963596
include Routes.url_helpers
35973597
def app; Routes end
35983598

3599-
test 'escapes generated path segment' do
3600-
assert_equal '/a%20b/c+d', segment_path(:segment => 'a b/c+d')
3599+
test 'escapes slash in generated path segment' do
3600+
assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d')
36013601
end
36023602

36033603
test 'unescapes recognized path segment' do
36043604
get '/a%20b%2Fc+d'
36053605
assert_equal 'a b/c+d', @response.body
36063606
end
36073607

3608-
test 'escapes generated path splat' do
3608+
test 'does not escape slash in generated path splat' do
36093609
assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d')
36103610
end
36113611

@@ -3790,6 +3790,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
37903790
get '/post(/:action(/:id))' => ok, as: :posts
37913791
get '/:foo/:foo_type/bars/:id' => ok, as: :bar
37923792
get '/projects/:id.:format' => ok, as: :project
3793+
get '/pages/:id' => ok, as: :page
3794+
get '/wiki/*page' => ok, as: :wiki
37933795
end
37943796
end
37953797

@@ -3822,6 +3824,26 @@ def app; Routes end
38223824
assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json)
38233825
assert_equal '/projects/1.json', project_path(1, :json)
38243826
end
3827+
3828+
test 'segments with question marks are escaped' do
3829+
assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar')
3830+
assert_equal '/pages/foo%3Fbar', page_path('foo?bar')
3831+
end
3832+
3833+
test 'segments with slashes are escaped' do
3834+
assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar')
3835+
assert_equal '/pages/foo%2Fbar', page_path('foo/bar')
3836+
end
3837+
3838+
test 'glob segments with question marks are escaped' do
3839+
assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar')
3840+
assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar')
3841+
end
3842+
3843+
test 'glob segments with slashes are not escaped' do
3844+
assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar')
3845+
assert_equal '/wiki/foo/bar', wiki_path('foo/bar')
3846+
end
38253847
end
38263848

38273849
class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest

actionpack/test/journey/router/utils_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ def test_path_escape
88
assert_equal "a/b%20c+d%25", Utils.escape_path("a/b c+d%")
99
end
1010

11+
def test_segment_escape
12+
assert_equal "a%2Fb%20c+d%25", Utils.escape_segment("a/b c+d%")
13+
end
14+
1115
def test_fragment_escape
1216
assert_equal "a/b%20c+d%25?e", Utils.escape_fragment("a/b c+d%?e")
1317
end

actionpack/test/journey/router_test.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,18 @@ def test_generate_escapes
367367
nil, { :controller => "tasks",
368368
:action => "a/b c+d",
369369
}, {})
370-
assert_equal '/tasks/a/b%20c+d', path
370+
assert_equal '/tasks/a%2Fb%20c+d', path
371+
end
372+
373+
def test_generate_escapes_with_namespaced_controller
374+
path = Path::Pattern.new '/:controller(/:action)'
375+
@router.routes.add_route @app, path, {}, {}, {}
376+
377+
path, _ = @formatter.generate(:path_info,
378+
nil, { :controller => "admin/tasks",
379+
:action => "a/b c+d",
380+
}, {})
381+
assert_equal '/admin/tasks/a%2Fb%20c+d', path
371382
end
372383

373384
def test_generate_extra_params

0 commit comments

Comments
 (0)