diff options
Diffstat (limited to 'activesupport/lib/active_support/core_ext/hash/conversions.rb')
-rw-r--r-- | activesupport/lib/active_support/core_ext/hash/conversions.rb | 177 |
1 files changed, 124 insertions, 53 deletions
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index e1ce9f371a..2149d4439d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections' class Hash # Returns a string containing an XML representation of its receiver: # - # {'foo' => 1, 'bar' => 2}.to_xml + # { foo: 1, bar: 2 }.to_xml # # => # # <?xml version="1.0" encoding="UTF-8"?> # # <hash> @@ -43,7 +43,10 @@ class Hash # end # # { foo: Foo.new }.to_xml(skip_instruct: true) - # # => "<hash><bar>fooing!</bar></hash>" + # # => + # # <hash> + # # <bar>fooing!</bar> + # # </hash> # # * Otherwise, a node with +key+ as tag is created with a string representation of # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added. @@ -101,56 +104,57 @@ class Hash # # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} - def from_xml(xml) - typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml))) + # + # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. + def from_xml(xml, disallowed_types = nil) + ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h + end + + # Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML. + def from_trusted_xml(xml) + from_xml xml, [] + end + end +end + +module ActiveSupport + class XMLConverter # :nodoc: + class DisallowedType < StandardError + def initialize(type) + super "Disallowed type attribute: #{type.inspect}" + end + end + + DISALLOWED_TYPES = %w(symbol yaml) + + def initialize(xml, disallowed_types = nil) + @xml = normalize_keys(XmlMini.parse(xml)) + @disallowed_types = disallowed_types || DISALLOWED_TYPES + end + + def to_h + deep_to_h(@xml) end private - def typecast_xml_value(value) - case value + def normalize_keys(params) + case params when Hash - if value['type'] == 'array' - _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) - if entries.nil? || (c = value['__content__'] && c.blank?) - [] - else - case entries # something weird with classes not matching here. maybe singleton methods breaking is_a? - when Array - entries.collect { |v| typecast_xml_value(v) } - when Hash - [typecast_xml_value(entries)] - else - raise "can't typecast #{entries.inspect}" - end - end - elsif value['type'] == 'file' || - (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?)) - content = value['__content__'] - if parser = ActiveSupport::XmlMini::PARSING[value['type']] - parser.arity == 1 ? parser.call(content) : parser.call(content, value) - else - content - end - elsif value['type'] == 'string' && value['nil'] != 'true' - '' - # blank or nil parsed values are represented by nil - elsif value.blank? || value['nil'] == 'true' - nil - # If the type is the only element which makes it then - # this still makes the value nil, except if type is - # a XML node(where type['value'] is a Hash) - elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash) - nil - else - xml_value = Hash[value.map { |k,v| [k, typecast_xml_value(v)] }] + Hash[params.map { |k,v| [k.to_s.tr('-', '_'), normalize_keys(v)] } ] + when Array + params.map { |v| normalize_keys(v) } + else + params + end + end - # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with - # how multipart uploaded files from HTML appear - xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value - end + def deep_to_h(value) + case value + when Hash + process_hash(value) when Array - value.map! { |i| typecast_xml_value(i) } - value.length > 1 ? value : value.first + process_array(value) when String value else @@ -158,15 +162,82 @@ class Hash end end - def unrename_keys(params) - case params - when Hash - Hash[params.map { |k,v| [k.to_s.tr('-', '_'), unrename_keys(v)] } ] - when Array - params.map { |v| unrename_keys(v) } + def process_hash(value) + if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type']) + raise DisallowedType, value['type'] + end + + if become_array?(value) + _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) + if entries.nil? || value['__content__'].try(:empty?) + [] else - params + case entries + when Array + entries.collect { |v| deep_to_h(v) } + when Hash + [deep_to_h(entries)] + else + raise "can't typecast #{entries.inspect}" + end + end + elsif become_content?(value) + process_content(value) + + elsif become_empty_string?(value) + '' + elsif become_hash?(value) + xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }] + + # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with + # how multipart uploaded files from HTML appear + xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value + end + end + + def become_content?(value) + value['type'] == 'file' || (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?)) + end + + def become_array?(value) + value['type'] == 'array' + end + + def become_empty_string?(value) + # { "string" => true } + # No tests fail when the second term is removed. + value['type'] == 'string' && value['nil'] != 'true' + end + + def become_hash?(value) + !nothing?(value) && !garbage?(value) + end + + def nothing?(value) + # blank or nil parsed values are represented by nil + value.blank? || value['nil'] == 'true' + end + + def garbage?(value) + # If the type is the only element which makes it then + # this still makes the value nil, except if type is + # an XML node(where type['value'] is a Hash) + value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 + end + + def process_content(value) + content = value['__content__'] + if parser = ActiveSupport::XmlMini::PARSING[value['type']] + parser.arity == 1 ? parser.call(content) : parser.call(content, value) + else + content end end + + def process_array(value) + value.map! { |i| deep_to_h(i) } + value.length > 1 ? value : value.first + end + end end |