diff --git a/History.md b/History.md index 3312c0a..1dc8ad0 100644 --- a/History.md +++ b/History.md @@ -6,6 +6,11 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are ## 2.6.0 / Master branch (not yet released) * ... +* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42] +* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42] +* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42] +* Fix security issue with map filter, see #230, #232, #234, #237 [Florian Weingarten, fw42] +* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42] * Bugfix for #106: fix example servlet [gnowoel] * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss] * Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice] diff --git a/README.md b/README.md index 695e9bb..481f18e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ * [Version history](History.md) * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics) * [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid) +* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Website](http://liquidmarkup.org/) ## Introduction diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index 88adb6b..f1bf48a 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -44,6 +44,10 @@ module Liquid true end + def inspect + self.class.to_s + end + def to_liquid self end @@ -54,7 +58,16 @@ module Liquid # Check for method existence without invoking respond_to?, which creates symbols def self.invokable?(method_name) - @invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s)) + unless @invokable_methods + # Ruby 1.8 compatibility: call to_s on method names (which are strings in 1.8, but already symbols in 1.9) + blacklist = (Liquid::Drop.public_instance_methods + [:each]).map(&:to_s) + if include?(Enumerable) + blacklist += Enumerable.public_instance_methods.map(&:to_s) + blacklist -= [:sort, :count, :first, :min, :max, :include?].map(&:to_s) + end + whitelist = [:to_liquid] + (public_instance_methods.map(&:to_s) - blacklist.map(&:to_s)) + @invokable_methods = Set.new(whitelist.map(&:to_s)) + end @invokable_methods.include?(method_name.to_s) end end diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 77bb945..b7f6418 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -81,7 +81,7 @@ module Liquid # Sort elements of the array # provide optional property with which to sort an array of hashes or drops def sort(input, property = nil) - ary = [input].flatten + ary = flatten_if_necessary(input) if property.nil? ary.sort elsif ary.first.respond_to?('[]') and !ary.first[property].nil? @@ -99,11 +99,14 @@ module Liquid # map/collect on a given property def map(input, property) - ary = [input].flatten - if ary.first.respond_to?('[]') and !ary.first[property].nil? - ary.map {|e| e[property] } - elsif ary.first.respond_to?(property) - ary.map {|e| e.send(property) } + flatten_if_necessary(input).map do |e| + e = e.call if e.is_a?(Proc) + + if property == "to_liquid" + e + elsif e.respond_to?(:[]) + e[property] + end end end @@ -244,6 +247,17 @@ module Liquid private + def flatten_if_necessary(input) + ary = if input.is_a?(Array) + input.flatten + elsif input.kind_of?(Enumerable) + input + else + [input].flatten + end + ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e } + end + def to_number(obj) case obj when Float diff --git a/liquid.gemspec b/liquid.gemspec index 0e909ca..e4b401e 100644 --- a/liquid.gemspec +++ b/liquid.gemspec @@ -12,6 +12,7 @@ Gem::Specification.new do |s| s.authors = ["Tobias Luetke"] s.email = ["tobi@leetsoft.com"] s.homepage = "http://www.liquidmarkup.org" + s.license = "MIT" #s.description = "A secure, non-evaling end user template engine with aesthetic markup." s.required_rubygems_version = ">= 1.3.7" diff --git a/test/liquid/drop_test.rb b/test/liquid/drop_test.rb index 7819f64..d8d02d9 100644 --- a/test/liquid/drop_test.rb +++ b/test/liquid/drop_test.rb @@ -55,11 +55,44 @@ class ProductDrop < Liquid::Drop end class EnumerableDrop < Liquid::Drop + def before_method(method) + method + end def size 3 end + def first + 1 + end + + def count + 3 + end + + def min + 1 + end + + def max + 3 + end + + def each + yield 1 + yield 2 + yield 3 + end +end + +class RealEnumerableDrop < Liquid::Drop + include Enumerable + + def before_method(method) + method + end + def each yield 1 yield 2 @@ -71,23 +104,34 @@ class DropsTest < Test::Unit::TestCase include Liquid def test_product_drop - assert_nothing_raised do tpl = Liquid::Template.parse( ' ' ) tpl.render('product' => ProductDrop.new) end end + def test_drop_does_only_respond_to_whitelisted_methods + assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render('product' => ProductDrop.new) + assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render('product' => ProductDrop.new) + assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render('product' => ProductDrop.new) + assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render('product' => ProductDrop.new) + assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render('product' => ProductDrop.new) + assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render('product' => ProductDrop.new) + end + + def test_drops_respond_to_to_liquid + assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render('product' => ProductDrop.new) + assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render('product' => ProductDrop.new) + end + def test_text_drop output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new) assert_equal ' text1 ', output - end def test_unknown_method output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new) assert_equal ' method: unknown ', output - end def test_integer_argument_drop @@ -159,6 +203,33 @@ class DropsTest < Test::Unit::TestCase assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new) end + def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names + ["select", "each", "map", "cycle"].each do |method| + assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new) + assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new) + assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) + assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) + end + end + + def test_some_enumerable_methods_still_get_invoked + [ :count, :max ].each do |method| + assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) + assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) + assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new) + assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new) + end + + assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render('collection' => RealEnumerableDrop.new) + + [ :min, :first ].each do |method| + assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new) + assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new) + assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new) + assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new) + end + end + def test_empty_string_value_access assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '') end diff --git a/test/liquid/standard_filter_test.rb b/test/liquid/standard_filter_test.rb index f971e72..b317b68 100644 --- a/test/liquid/standard_filter_test.rb +++ b/test/liquid/standard_filter_test.rb @@ -6,6 +6,39 @@ class Filters include Liquid::StandardFilters end +class TestThing + def initialize + @foo = 0 + end + + def to_s + "woot: #{@foo}" + end + + def [](whatever) + to_s + end + + def to_liquid + @foo += 1 + self + end +end + +class TestDrop < Liquid::Drop + def test + "testfoo" + end +end + +class TestEnumerable < Liquid::Drop + include Enumerable + + def each(&block) + [ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block) + end +end + class StandardFiltersTest < Test::Unit::TestCase include Liquid @@ -97,6 +130,36 @@ class StandardFiltersTest < Test::Unit::TestCase 'ary' => [{'foo' => {'bar' => 'a'}}, {'foo' => {'bar' => 'b'}}, {'foo' => {'bar' => 'c'}}] end + def test_map_doesnt_call_arbitrary_stuff + assert_equal "", Liquid::Template.parse('{{ "foo" | map: "__id__" }}').render + assert_equal "", Liquid::Template.parse('{{ "foo" | map: "inspect" }}').render + end + + def test_map_calls_to_liquid + t = TestThing.new + assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t]) + end + + def test_sort_calls_to_liquid + t = TestThing.new + assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) + end + + def test_map_over_proc + drop = TestDrop.new + p = Proc.new{ drop } + templ = '{{ procs | map: "test" }}' + assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p]) + end + + def test_map_works_on_enumerables + assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new) + end + + def test_sort_works_on_enumerables + assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new) + end + def test_date assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")