diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index e334677..129b71a 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -39,6 +39,7 @@ module Liquid filters.each do |f| raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) + Strainer.add_known_filter(f) strainer.extend(f) end end diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index 3972d08..88adb6b 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -1,3 +1,5 @@ +require 'set' + module Liquid # A drop in liquid is a class which allows you to export DOM like things to liquid. @@ -31,8 +33,8 @@ module Liquid # called by liquid to invoke a drop def invoke_drop(method_or_key) - if method_or_key && method_or_key != EMPTY_STRING && drop_method_defined?(method_or_key.to_s) - send(method_or_key.to_s) + if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key) + send(method_or_key) else before_method(method_or_key) end @@ -51,8 +53,9 @@ module Liquid private # Check for method existence without invoking respond_to?, which creates symbols - def drop_method_defined?(method_name) - self.class.public_instance_methods.any? {|method| method.to_s == method_name } + def self.invokable?(method_name) + @invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s)) + @invokable_methods.include?(method_name.to_s) end end end diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb index 79f9ca7..15aefa4 100644 --- a/lib/liquid/strainer.rb +++ b/lib/liquid/strainer.rb @@ -2,19 +2,15 @@ require 'set' module Liquid - parent_object = if defined? BlankObject - BlankObject - else - Object - end - # Strainer is the parent class for the filters system. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # Context#add_filters or Template.register_filter - class Strainer < parent_object #:nodoc: + class Strainer #:nodoc: @@filters = {} + @@known_filters = Set.new + @@known_methods = Set.new def initialize(context) @context = context @@ -22,9 +18,20 @@ module Liquid def self.global_filter(filter) raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) + add_known_filter(filter) @@filters[filter.name] = filter end + def self.add_known_filter(filter) + unless @@known_filters.include?(filter) + @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s)) + new_methods = filter.instance_methods.map(&:to_s) + new_methods.reject!{ |m| @@method_blacklist.include?(m) } + @@known_methods.merge(new_methods) + @@known_filters.add(filter) + end + end + def self.create(context) strainer = Strainer.new(context) @@filters.each { |k,m| strainer.extend(m) } @@ -32,21 +39,15 @@ module Liquid end def invoke(method, *args) - if has_method?(method) + if invokable?(method) send(method, *args) else args.first end end - private - - def has_method?(method) - methods_to_check = self.methods - self.class.public_instance_methods - methods_to_check.any? do |instance_method| - instance_method.to_s == method.to_s - end + def invokable?(method) + @@known_methods.include?(method.to_s) && respond_to?(method) end - end end diff --git a/test/liquid/drop_test.rb b/test/liquid/drop_test.rb index ebac1e5..7819f64 100644 --- a/test/liquid/drop_test.rb +++ b/test/liquid/drop_test.rb @@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase assert_equal ' ', output end + def test_object_methods_not_allowed + [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| + output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new) + assert_equal ' ', output + end + end + def test_scope assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) diff --git a/test/liquid/strainer_test.rb b/test/liquid/strainer_test.rb index 7e759f7..582ed7f 100644 --- a/test/liquid/strainer_test.rb +++ b/test/liquid/strainer_test.rb @@ -14,22 +14,21 @@ class StrainerTest < Test::Unit::TestCase private :private_filter end - module TestingFilter - def test1 - "test1" - end - - def test2 - "test2" - end - end - Strainer.global_filter(AccessScopeFilters) - Strainer.global_filter(TestingFilter) + + def test_strainer + strainer = Strainer.create(nil) + assert_equal 5, strainer.invoke('size', 'input') + assert_equal "public", strainer.invoke("public_filter") + end def test_strainer_only_invokes_public_filter_methods strainer = Strainer.create(nil) - assert_equal "public", strainer.invoke("public_filter") + assert_equal false, strainer.invokable?('__test__') + assert_equal false, strainer.invokable?('test') + assert_equal false, strainer.invokable?('instance_eval') + assert_equal false, strainer.invokable?('__send__') + assert_equal true, strainer.invokable?('size') # from the standard lib end def test_strainer_returns_nil_if_no_filter_method_found @@ -43,12 +42,6 @@ class StrainerTest < Test::Unit::TestCase assert_equal "password", strainer.invoke("undef_the_method", "password") end - def test_strainer_allows_multiple_filters - strainer = Strainer.create(nil) - assert_equal "test1", strainer.invoke("test1") - assert_equal "test2", strainer.invoke("test2") - end - def test_strainer_only_allows_methods_defined_in_filters strainer = Strainer.create(nil) assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")