Skip to content

Commit 496d744

Browse files
committed
Allow specifying encoding of parameters by action
At GitHub we need to handle parameter encodings that are not UTF-8. This patch allows us to specify encodings per parameter per action.
1 parent 46f5116 commit 496d744

File tree

10 files changed

+139
-20
lines changed

10 files changed

+139
-20
lines changed

actionpack/lib/action_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module ActionController
4040
autoload :Rescue
4141
autoload :Streaming
4242
autoload :StrongParameters
43+
autoload :ParameterEncoding
4344
autoload :Testing
4445
autoload :UrlFor
4546
end

actionpack/lib/action_controller/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def self.without_modules(*modules)
217217
MimeResponds,
218218
ImplicitRender,
219219
StrongParameters,
220-
220+
ParameterEncoding,
221221
Cookies,
222222
Flash,
223223
FormBuilder,

actionpack/lib/action_controller/metal.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ def self.make_response!(request)
139139
end
140140
end
141141

142+
def self.encoding_for_param(action, param) # :nodoc:
143+
::Encoding::UTF_8
144+
end
145+
142146
# Delegates to the class' <tt>controller_name</tt>
143147
def controller_name
144148
self.class.controller_name
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module ActionController
2+
module ParameterEncoding
3+
extend ActiveSupport::Concern
4+
5+
module ClassMethods
6+
def inherited(klass)
7+
super
8+
klass.setup_param_encode
9+
end
10+
11+
def setup_param_encode
12+
@_parameter_encodings = {}
13+
end
14+
15+
def encoding_for_param(action, param)
16+
if @_parameter_encodings[action.to_s] && @_parameter_encodings[action.to_s][param.to_s]
17+
@_parameter_encodings[action.to_s][param.to_s]
18+
else
19+
::Encoding::UTF_8
20+
end
21+
end
22+
23+
def parameter_encoding(action, param_name, encoding)
24+
@_parameter_encodings[action.to_s] ||= {}
25+
@_parameter_encodings[action.to_s][param_name.to_s] = encoding
26+
end
27+
end
28+
end
29+
end

actionpack/lib/action_controller/test_case.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,27 @@ def self.new_session
3333
TestSession.new
3434
end
3535

36+
attr_reader :controller_class
37+
3638
# Create a new test request with default `env` values
37-
def self.create
39+
def self.create(controller_class)
3840
env = {}
3941
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
4042
env["rack.request.cookie_hash"] = {}.with_indifferent_access
41-
new(default_env.merge(env), new_session)
43+
new(default_env.merge(env), new_session, controller_class)
4244
end
4345

4446
def self.default_env
4547
DEFAULT_ENV
4648
end
4749
private_class_method :default_env
4850

49-
def initialize(env, session)
51+
def initialize(env, session, controller_class)
5052
super(env)
5153

5254
self.session = session
5355
self.session_options = TestSession::DEFAULT_OPTIONS
56+
@controller_class = controller_class
5457
@custom_param_parsers = {
5558
xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
5659
}
@@ -497,7 +500,7 @@ def process(action, *args)
497500
@request.set_header "HTTP_COOKIE", cookies.to_header
498501
@request.delete_header "action_dispatch.cookies"
499502

500-
@request = TestRequest.new scrub_env!(@request.env), @request.session
503+
@request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
501504
@response = build_response @response_klass
502505
@response.request = @request
503506
@controller.recycle!
@@ -591,7 +594,7 @@ def setup_controller_request_and_response
591594
end
592595
end
593596

594-
@request = TestRequest.create
597+
@request = TestRequest.create(@controller.class)
595598
@response = build_response @response_klass
596599
@response.request = @request
597600

actionpack/lib/action_dispatch/http/parameters.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,23 @@ def parameters
3737
query_parameters.dup
3838
end
3939
params.merge!(path_parameters)
40+
params = set_custom_encoding(params)
4041
set_header("action_dispatch.request.parameters", params)
4142
params
4243
end
4344
alias :params :parameters
4445

46+
def set_custom_encoding(params)
47+
action = params[:action]
48+
params.each do |k, v|
49+
if v.is_a?(String) && v.encoding != encoding_template(action, k)
50+
params[k] = v.force_encoding(encoding_template(action, k))
51+
end
52+
end
53+
54+
params
55+
end
56+
4557
def path_parameters=(parameters) #:nodoc:
4658
delete_header("action_dispatch.request.parameters")
4759

@@ -64,6 +76,10 @@ def path_parameters
6476

6577
private
6678

79+
def encoding_template(action, param)
80+
controller_class.encoding_for_param(action, param)
81+
end
82+
6783
def parse_formatted_parameters(parsers)
6884
return yield if content_length.zero? || content_mime_type.nil?
6985

actionpack/lib/action_dispatch/http/request.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def commit_cookie_jar! # :nodoc:
6969
PASS_NOT_FOUND = Class.new { # :nodoc:
7070
def self.action(_); self; end
7171
def self.call(_); [404, {"X-Cascade" => "pass"}, []]; end
72+
def self.encoding_for_param(action, param); ::Encoding::UTF_8; end
7273
}
7374

7475
def controller_class

actionpack/lib/action_dispatch/testing/assertions/routing.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def recognized_request_for(path, extras = {}, msg)
184184
end
185185

186186
# Assume given controller
187-
request = ActionController::TestRequest.create
187+
request = ActionController::TestRequest.create @controller.class
188188

189189
if path =~ %r{://}
190190
fail_on(URI::InvalidURIError, msg) do

actionpack/test/abstract_unit.rb

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,27 +122,19 @@ class DeadEndRoutes < ActionDispatch::Routing::RouteSet
122122
# Stub Rails dispatcher so it does not get controller references and
123123
# simply return the controller#action as Rack::Body.
124124
class NullController < ::ActionController::Metal
125-
def initialize(controller_name)
126-
@controller = controller_name
127-
end
128-
129-
def make_response!(request)
130-
self.class.make_response! request
131-
end
132-
133-
def dispatch(action, req, res)
134-
[200, {"Content-Type" => "text/html"}, ["#{@controller}##{action}"]]
125+
def self.dispatch(action, req, res)
126+
[200, {"Content-Type" => "text/html"}, ["#{req.params[:controller]}##{action}"]]
135127
end
136128
end
137129

138-
class NullControllerRequest < DelegateClass(ActionDispatch::Request)
130+
class NullControllerRequest < ActionDispatch::Request
139131
def controller_class
140-
NullController.new params[:controller]
132+
NullController
141133
end
142134
end
143135

144136
def make_request(env)
145-
NullControllerRequest.new super
137+
NullControllerRequest.new env
146138
end
147139
end
148140

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
require "abstract_unit"
2+
3+
class ParameterEncodingController < ActionController::Base
4+
parameter_encoding :test_bar, :bar, Encoding::ASCII_8BIT
5+
parameter_encoding :test_baz, :baz, Encoding::ISO_8859_1
6+
parameter_encoding :test_baz_to_ascii, :baz, Encoding::ASCII_8BIT
7+
8+
def test_foo
9+
render body: params[:foo].encoding
10+
end
11+
12+
def test_bar
13+
render body: params[:bar].encoding
14+
end
15+
16+
def test_baz
17+
render body: params[:baz].encoding
18+
end
19+
20+
def test_no_change_to_baz
21+
render body: params[:baz].encoding
22+
end
23+
24+
def test_baz_to_ascii
25+
render body: params[:baz].encoding
26+
end
27+
end
28+
29+
class ParameterEncodingTest < ActionController::TestCase
30+
tests ParameterEncodingController
31+
32+
test "properly transcodes UTF8 parameters into declared encodings" do
33+
post :test_foo, params: {"foo" => "foo", "bar" => "bar", "baz" => "baz"}
34+
35+
assert_response :success
36+
assert_equal "UTF-8", @response.body
37+
end
38+
39+
test "properly transcodes ASCII_8BIT parameters into declared encodings" do
40+
post :test_bar, params: {"foo" => "foo", "bar" => "bar", "baz" => "baz"}
41+
42+
assert_response :success
43+
assert_equal "ASCII-8BIT", @response.body
44+
end
45+
46+
test "properly transcodes ISO_8859_1 parameters into declared encodings" do
47+
post :test_baz, params: {"foo" => "foo", "bar" => "bar", "baz" => "baz"}
48+
49+
assert_response :success
50+
assert_equal "ISO-8859-1", @response.body
51+
end
52+
53+
test "does not transcode parameters when not specified" do
54+
post :test_no_change_to_baz, params: {"foo" => "foo", "bar" => "bar", "baz" => "baz"}
55+
56+
assert_response :success
57+
assert_equal "UTF-8", @response.body
58+
end
59+
60+
test "respects different encoding declarations for a param per action" do
61+
post :test_baz_to_ascii, params: {"foo" => "foo", "bar" => "bar", "baz" => "baz"}
62+
63+
assert_response :success
64+
assert_equal "ASCII-8BIT", @response.body
65+
end
66+
67+
test "does not raise an error when passed a param declared as ASCII-8BIT that contains invalid bytes" do
68+
get :test_bar, params: { "bar" => URI.parser.escape("bar\xE2baz".b) }
69+
70+
assert_response :success
71+
assert_equal "ASCII-8BIT", @response.body
72+
end
73+
end

0 commit comments

Comments
 (0)