Browse files

Extract XmlParamsParser out from Action Pack

  • Loading branch information...
0 parents commit 4546cb414acd9f05254643df8f83ef846fb3f851 @sikachu sikachu committed Feb 19, 2013
17 .gitignore
@@ -0,0 +1,17 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
7 Gemfile
@@ -0,0 +1,7 @@
+source :rubygems
+
+gemspec
+
+git 'git://github.com/rails/rails.git', branch: 'master' do
+ gem 'actionpack'
+end
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2013 Prem Sichanugrist
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 README.md
@@ -0,0 +1,16 @@
+actionpack-xml\_parser
+======================
+
+A XML parameters parser for Action Pack (removed from core in Rails 4.0)
+
+Installation
+------------
+
+Include this gem into your Gemfile:
+
+ gem 'actionpack-xml_parser', github: 'rails/actionpack-xml_parser'
+
+Then, add `ActionDispatch::XmlParamsParser` middleware after `ActionDispatch::ParamsParser`
+in `config/application.rb`:
+
+ config.middleware.insert_after ActionDispatch::ParamsParser, ActionDispatch::XmlParamsParser
11 Rakefile
@@ -0,0 +1,11 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
+require 'rake/testtask'
+
+Rake::TestTask.new do |t|
+ t.libs = ["test"]
+ t.pattern = "test/**/*_test.rb"
+ t.ruby_opts = ['-w']
+end
+
+task :default => :test
21 actionpack-xml_parser.gemspec
@@ -0,0 +1,21 @@
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'actionpack-xml_parser'
+ s.version = '0.1.0'
+ s.summary = 'XML parameters parser for Action Pack (removed from core in Rails 4.0)'
+
+ s.required_ruby_version = '>= 1.9.3'
+ s.license = 'MIT'
+
+ s.author = 'Prem Sichanugrist'
+ s.email = 's@sikac.hu'
+ s.homepage = 'http://www.rubyonrails.org'
+
+ s.files = Dir['LICENSE', 'README.md', 'lib/**/*']
+ s.require_path = 'lib'
+
+ s.extra_rdoc_files = %w( README.md )
+ s.rdoc_options.concat ['--main', 'README.md']
+
+ s.add_dependency('actionpack', '~> 4.0.0.beta')
+end
50 lib/action_dispatch/xml_params_parser.rb
@@ -0,0 +1,50 @@
+require 'active_support/core_ext/hash/conversions'
+require 'action_dispatch/http/request'
+require 'active_support/core_ext/hash/indifferent_access'
+
+module ActionDispatch
+ class XmlParamsParser
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if params = parse_formatted_parameters(env)
+ env["action_dispatch.request.request_parameters"] = params
+ end
+
+ @app.call(env)
+ end
+
+ private
+ def parse_formatted_parameters(env)
+ request = Request.new(env)
+
+ return false if request.content_length.zero?
+
+ mime_type = content_type_from_legacy_post_data_format_header(env) ||
+ request.content_mime_type
+
+ if mime_type == Mime::XML
+ data = request.deep_munge(Hash.from_xml(request.body.read) || {})
+ data.with_indifferent_access
+ else
+ false
+ end
+ rescue Exception => e # YAML, XML or Ruby code block errors
+ logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
+
+ raise ActionDispatch::ParamsParser::ParseError.new(e.message, e)
+ end
+
+ def content_type_from_legacy_post_data_format_header(env)
+ if env['HTTP_X_POST_DATA_FORMAT'].to_s.downcase == 'xml'
+ Mime::XML
+ end
+ end
+
+ def logger(env)
+ env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr)
+ end
+ end
+end
1 lib/actionpack/xml_parser.rb
@@ -0,0 +1 @@
+require 'action_dispatch/xml_params_parser'
1 test/fixtures/500.html
@@ -0,0 +1 @@
+500 error fixture
82 test/helper.rb
@@ -0,0 +1,82 @@
+require 'bundler/setup'
+
+require 'active_support/testing/autorun'
+require 'action_controller'
+require 'action_dispatch'
+require 'action_dispatch/xml_params_parser'
+
+FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
+
+module ActionDispatch
+ module SharedRoutes
+ def before_setup
+ @routes = SharedTestRoutes
+ super
+ end
+ end
+end
+
+class RoutedRackApp
+ attr_reader :routes
+
+ def initialize(routes, &blk)
+ @routes = routes
+ @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
+ end
+
+ def call(env)
+ @stack.call(env)
+ end
+end
+
+class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
+ include ActionDispatch::SharedRoutes
+
+ def self.build_app(routes = nil)
+ RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
+ middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new(FIXTURE_LOAD_PATH)
+ middleware.use "ActionDispatch::ParamsParser"
+ middleware.use "ActionDispatch::XmlParamsParser"
+ middleware.use "Rack::Head"
+ yield(middleware) if block_given?
+ end
+ end
+
+ self.app = build_app
+
+ # Stub Rails dispatcher so it does not get controller references and
+ # simply return the controller#action as Rack::Body.
+ class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
+ protected
+ def controller_reference(controller_param)
+ controller_param
+ end
+
+ def dispatch(controller, action, env)
+ [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]]
+ end
+ end
+
+ def self.stub_controllers
+ old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
+ ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
+ ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
+ yield ActionDispatch::Routing::RouteSet.new
+ ensure
+ ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
+ ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
+ end
+
+ def with_routing(&block)
+ temporary_routes = ActionDispatch::Routing::RouteSet.new
+ old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes)
+ old_routes = SharedTestRoutes
+ silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) }
+
+ yield temporary_routes
+ ensure
+ self.class.app = old_app
+ silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
+ end
+end
219 test/webservice_test.rb
@@ -0,0 +1,219 @@
+require 'helper'
+
+class WebServiceTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ def assign_parameters
+ if params[:full]
+ render :text => dump_params_keys
+ else
+ render :text => (params.keys - ['controller', 'action']).sort.join(", ")
+ end
+ end
+
+ def dump_params_keys(hash = params)
+ hash.keys.sort.inject("") do |s, k|
+ value = hash[k]
+ value = Hash === value ? "(#{dump_params_keys(value)})" : ""
+ s << ", " unless s.empty?
+ s << "#{k}#{value}"
+ end
+ end
+ end
+
+ def setup
+ @controller = TestController.new
+ @integration_session = nil
+ end
+
+ def test_check_parameters
+ with_test_route_set do
+ get "/"
+ assert_equal '', @controller.response.body
+ end
+ end
+
+ def test_post_xml
+ with_test_route_set do
+ post "/", '<entry attributed="true"><summary>content...</summary></entry>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'entry', @controller.response.body
+ assert @controller.params.has_key?(:entry)
+ assert_equal 'content...', @controller.params["entry"]['summary']
+ assert_equal 'true', @controller.params["entry"]['attributed']
+ end
+ end
+
+ def test_put_xml
+ with_test_route_set do
+ put "/", '<entry attributed="true"><summary>content...</summary></entry>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'entry', @controller.response.body
+ assert @controller.params.has_key?(:entry)
+ assert_equal 'content...', @controller.params["entry"]['summary']
+ assert_equal 'true', @controller.params["entry"]['attributed']
+ end
+ end
+
+ def test_put_xml_using_a_type_node
+ with_test_route_set do
+ put "/", '<type attributed="true"><summary>content...</summary></type>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'type', @controller.response.body
+ assert @controller.params.has_key?(:type)
+ assert_equal 'content...', @controller.params["type"]['summary']
+ assert_equal 'true', @controller.params["type"]['attributed']
+ end
+ end
+
+ def test_put_xml_using_a_type_node_and_attribute
+ with_test_route_set do
+ put "/", '<type attributed="true"><summary type="boolean">false</summary></type>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'type', @controller.response.body
+ assert @controller.params.has_key?(:type)
+ assert_equal false, @controller.params["type"]['summary']
+ assert_equal 'true', @controller.params["type"]['attributed']
+ end
+ end
+
+ def test_post_xml_using_a_type_node
+ with_test_route_set do
+ post "/", '<font attributed="true"><type>arial</type></font>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'font', @controller.response.body
+ assert @controller.params.has_key?(:font)
+ assert_equal 'arial', @controller.params['font']['type']
+ assert_equal 'true', @controller.params["font"]['attributed']
+ end
+ end
+
+ def test_post_xml_using_a_root_node_named_type
+ with_test_route_set do
+ post "/", '<type type="integer">33</type>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert @controller.params.has_key?(:type)
+ assert_equal 33, @controller.params['type']
+ end
+ end
+
+ def test_post_xml_using_an_attributted_node_named_type
+ with_test_route_set do
+ with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do
+ post "/", '<request><type type="string">Arial,12</type><z>3</z></request>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'type, z', @controller.response.body
+ assert @controller.params.has_key?(:type)
+ assert_equal 'Arial,12', @controller.params['type'], @controller.params.inspect
+ assert_equal '3', @controller.params['z'], @controller.params.inspect
+ end
+ end
+ end
+
+ def test_post_xml_using_a_disallowed_type_attribute
+ $stderr = StringIO.new
+ with_test_route_set do
+ post '/', '<foo type="symbol">value</foo>', 'CONTENT_TYPE' => 'application/xml'
+ assert_response 500
+
+ post '/', '<foo type="yaml">value</foo>', 'CONTENT_TYPE' => 'application/xml'
+ assert_response 500
+ end
+ ensure
+ $stderr = STDERR
+ end
+
+ def test_register_and_use_xml_simple
+ with_test_route_set do
+ with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do
+ post "/", '<request><summary>content...</summary><title>SimpleXml</title></request>',
+ {'CONTENT_TYPE' => 'application/xml'}
+
+ assert_equal 'summary, title', @controller.response.body
+ assert @controller.params.has_key?(:summary)
+ assert @controller.params.has_key?(:title)
+ assert_equal 'content...', @controller.params["summary"]
+ assert_equal 'SimpleXml', @controller.params["title"]
+ end
+ end
+ end
+
+ def test_use_xml_ximple_with_empty_request
+ with_test_route_set do
+ assert_nothing_raised { post "/", "", {'CONTENT_TYPE' => 'application/xml'} }
+ assert_equal '', @controller.response.body
+ end
+ end
+
+ def test_dasherized_keys_as_xml
+ with_test_route_set do
+ post "/?full=1", "<first-key>\n<sub-key>...</sub-key>\n</first-key>",
+ {'CONTENT_TYPE' => 'application/xml'}
+ assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body
+ assert_equal "...", @controller.params[:first_key][:sub_key]
+ end
+ end
+
+ def test_typecast_as_xml
+ with_test_route_set do
+ xml = <<-XML
+ <data>
+ <a type="integer">15</a>
+ <b type="boolean">false</b>
+ <c type="boolean">true</c>
+ <d type="date">2005-03-17</d>
+ <e type="datetime">2005-03-17T21:41:07Z</e>
+ <f>unparsed</f>
+ <g type="integer">1</g>
+ <g>hello</g>
+ <g type="date">1974-07-25</g>
+ </data>
+ XML
+ post "/", xml, {'CONTENT_TYPE' => 'application/xml'}
+
+ params = @controller.params
+ assert_equal 15, params[:data][:a]
+ assert_equal false, params[:data][:b]
+ assert_equal true, params[:data][:c]
+ assert_equal Date.new(2005,3,17), params[:data][:d]
+ assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e]
+ assert_equal "unparsed", params[:data][:f]
+ assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g]
+ end
+ end
+
+ def test_entities_unescaped_as_xml_simple
+ with_test_route_set do
+ xml = <<-XML
+ <data>&lt;foo &quot;bar&apos;s&quot; &amp; friends&gt;</data>
+ XML
+ post "/", xml, {'CONTENT_TYPE' => 'application/xml'}
+ assert_equal %(<foo "bar's" & friends>), @controller.params[:data]
+ end
+ end
+
+ private
+ def with_params_parsers(parsers = {})
+ old_session = @integration_session
+ @app = ActionDispatch::ParamsParser.new(app.routes, parsers)
+ reset!
+ yield
+ ensure
+ @integration_session = old_session
+ end
+
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ match '/', :to => 'web_service_test/test#assign_parameters', :via => :all
+ end
+ yield
+ end
+ end
+end
182 test/xml_params_parsing_test.rb
@@ -0,0 +1,182 @@
+require 'helper'
+
+class XmlParamsParsingTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ class << self
+ attr_accessor :last_request_parameters
+ end
+
+ def parse
+ self.class.last_request_parameters = request.request_parameters
+ head :ok
+ end
+ end
+
+ def teardown
+ TestController.last_request_parameters = nil
+ end
+
+ test "parses a strict rack.input" do
+ class Linted
+ undef call if method_defined?(:call)
+ def call(env)
+ bar = env['action_dispatch.request.request_parameters']['foo']
+ result = "<ok>#{bar}</ok>"
+ [200, {"Content-Type" => "application/xml", "Content-Length" => result.length.to_s}, [result]]
+ end
+ end
+ req = Rack::MockRequest.new(ActionDispatch::XmlParamsParser.new(Linted.new))
+ resp = req.post('/', "CONTENT_TYPE" => "application/xml", :input => "<foo>bar</foo>", :lint => true)
+ assert_equal "<ok>bar</ok>", resp.body
+ end
+
+ def assert_parses(expected, xml)
+ with_test_routing do
+ post "/parse", xml, default_headers
+ assert_response :ok
+ assert_equal(expected, TestController.last_request_parameters)
+ end
+ end
+
+ test "nils are stripped from collections" do
+ assert_parses(
+ {"hash" => { "person" => nil} },
+ "<hash><person type=\"array\"><person nil=\"true\"/></person></hash>")
+ assert_parses(
+ {"hash" => { "person" => ['foo']} },
+ "<hash><person type=\"array\"><person>foo</person><person nil=\"true\"/></person>\n</hash>")
+ end
+
+ test "parses hash params" do
+ with_test_routing do
+ xml = "<person><name>David</name></person>"
+ post "/parse", xml, default_headers
+ assert_response :ok
+ assert_equal({"person" => {"name" => "David"}}, TestController.last_request_parameters)
+ end
+ end
+
+ test "parses single file" do
+ with_test_routing do
+ xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></person>"
+ post "/parse", xml, default_headers
+ assert_response :ok
+
+ person = TestController.last_request_parameters
+ assert_equal "image/jpg", person['person']['avatar'].content_type
+ assert_equal "me.jpg", person['person']['avatar'].original_filename
+ assert_equal "ABC", person['person']['avatar'].read
+ end
+ end
+
+ test "logs error if parsing unsuccessful" do
+ with_test_routing do
+ output = StringIO.new
+ xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></pineapple>"
+ post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output))
+ assert_response :error
+ output.rewind && err = output.read
+ assert err =~ /Error occurred while parsing request parameters/
+ end
+ end
+
+ test "occurring a parse error if parsing unsuccessful" do
+ with_test_routing do
+ begin
+ $stderr = StringIO.new # suppress the log
+ xml = "<person><name>David</name></pineapple>"
+ exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => false) }
+ assert_equal REXML::ParseException, exception.original_exception.class
+ assert_equal exception.original_exception.message, exception.message
+ ensure
+ $stderr = STDERR
+ end
+ end
+ end
+
+ test "parses multiple files" do
+ xml = <<-end_body
+ <person>
+ <name>David</name>
+ <avatars>
+ <avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar>
+ <avatar type='file' name='you.gif' content_type='image/gif'>#{::Base64.encode64('DEF')}</avatar>
+ </avatars>
+ </person>
+ end_body
+
+ with_test_routing do
+ post "/parse", xml, default_headers
+ assert_response :ok
+ end
+
+ person = TestController.last_request_parameters
+
+ assert_equal "image/jpg", person['person']['avatars']['avatar'].first.content_type
+ assert_equal "me.jpg", person['person']['avatars']['avatar'].first.original_filename
+ assert_equal "ABC", person['person']['avatars']['avatar'].first.read
+
+ assert_equal "image/gif", person['person']['avatars']['avatar'].last.content_type
+ assert_equal "you.gif", person['person']['avatars']['avatar'].last.original_filename
+ assert_equal "DEF", person['person']['avatars']['avatar'].last.read
+ end
+
+ private
+ def with_test_routing
+ with_routing do |set|
+ set.draw do
+ post ':action', :to => ::XmlParamsParsingTest::TestController
+ end
+ yield
+ end
+ end
+
+ def default_headers
+ {'CONTENT_TYPE' => 'application/xml'}
+ end
+end
+
+class LegacyXmlParamsParsingTest < XmlParamsParsingTest
+ private
+ def default_headers
+ {'HTTP_X_POST_DATA_FORMAT' => 'xml'}
+ end
+end
+
+class RootLessXmlParamsParsingTest < ActionDispatch::IntegrationTest
+ class TestController < ActionController::Base
+ wrap_parameters :person, :format => :xml
+
+ class << self
+ attr_accessor :last_request_parameters
+ end
+
+ def parse
+ self.class.last_request_parameters = request.request_parameters
+ head :ok
+ end
+ end
+
+ def teardown
+ TestController.last_request_parameters = nil
+ end
+
+ test "parses hash params" do
+ with_test_routing do
+ xml = "<name>David</name>"
+ post "/parse", xml, {'CONTENT_TYPE' => 'application/xml'}
+ assert_response :ok
+ assert_equal({"name" => "David", "person" => {"name" => "David"}}, TestController.last_request_parameters)
+ end
+ end
+
+ private
+ def with_test_routing
+ with_routing do |set|
+ set.draw do
+ post ':action', :to => ::RootLessXmlParamsParsingTest::TestController
+ end
+ yield
+ end
+ end
+end

0 comments on commit 4546cb4

Please sign in to comment.