Skip to content

Commit

Permalink
Disable rendering of tag based on register (Shopify#1162)
Browse files Browse the repository at this point in the history
* Disable rendering of tag based on register

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

* Fix array handling shortcut in disable_tags
  • Loading branch information
shopmike authored Sep 25, 2019
1 parent f4d134c commit 0db9c56
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 8 deletions.
2 changes: 2 additions & 0 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ module Liquid
require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers'

# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }
13 changes: 12 additions & 1 deletion lib/liquid/block_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,25 @@ def render_to_output_buffer(context, output)
private

def render_node(context, output, node)
node.render_to_output_buffer(context, output)
if node.disabled?(context)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end

def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers['disabled_tags'].disable(tags, &block)
end

def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
Expand Down
2 changes: 2 additions & 0 deletions lib/liquid/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"
6 changes: 6 additions & 0 deletions lib/liquid/register.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module Liquid
class Register
end
end
32 changes: 32 additions & 0 deletions lib/liquid/registers/disabled_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Liquid
class DisabledTags < Register
def initialize
@disabled_tags = {}
end

def disabled?(tag)
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
end

def disable(tags)
tags.each(&method(:increment))
yield
ensure
tags.each(&method(:decrement))
end

private

def increment(tag)
@disabled_tags[tag] ||= 0
@disabled_tags[tag] += 1
end

def decrement(tag)
@disabled_tags[tag] -= 1
end
end

Template.add_register('disabled_tags', DisabledTags.new)
end
20 changes: 20 additions & 0 deletions lib/liquid/tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ def parse(tag_name, markup, tokenizer, parse_context)
tag
end

def disable_tags(*tags)
disabled_tags.push(*tags)
end

private :new

def disabled_tags
@disabled_tags ||= []
end
end

def initialize(tag_name, markup, parse_context)
Expand All @@ -38,6 +46,14 @@ def render(_context)
''
end

def disabled?(context)
context.registers['disabled_tags'].disabled?(tag_name)
end

def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end

# For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
Expand All @@ -49,5 +65,9 @@ def render_to_output_buffer(context, output)
def blank?
false
end

def disabled_tags
self.class.disabled_tags
end
end
end
6 changes: 6 additions & 0 deletions lib/liquid/tags/render.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Liquid
class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o

disable_tags "include"

attr_reader :template_name_expr, :attributes

def initialize(tag_name, markup, options)
Expand All @@ -22,6 +24,10 @@ def initialize(tag_name, markup, options)
end

def render_to_output_buffer(context, output)
render_tag(context, output)
end

def render_tag(context, output)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
Expand Down
18 changes: 17 additions & 1 deletion lib/liquid/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ def tags
@tags ||= TagRegistry.new
end

def add_register(name, klass)
registers[name.to_s] = klass
end

def registers
@registers ||= {}
end

def error_mode
@error_mode ||= :lax
end
Expand Down Expand Up @@ -191,18 +199,26 @@ def render(*args)

output = nil

context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers

case args.last
when Hash
options = args.pop
output = options[:output] if options[:output]

registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
options[:registers]&.each do |key, register|
context_register[key] = register
end

apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end

Template.registers.each do |key, register|
context_register[key] = register
end

# Retrying a render resets resource usage
context.resource_limits.reset

Expand Down
8 changes: 8 additions & 0 deletions lib/liquid/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ def render_to_output_buffer(context, output)
output
end

def disabled?(_context)
false
end

def disabled_tags
[]
end

private

def parse_filter_expressions(filter_name, unparsed_args)
Expand Down
27 changes: 27 additions & 0 deletions test/integration/registers/disabled_tags_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

require 'test_helper'

class DisabledTagsTest < Minitest::Test
include Liquid

class DisableRaw < Block
disable_tags "raw"
end

class DisableRawEcho < Block
disable_tags "raw", "echo"
end

def test_disables_raw
with_custom_tag('disable', DisableRaw) do
assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end

def test_disables_echo_and_raw
with_custom_tag('disable', DisableRawEcho) do
assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
end
29 changes: 23 additions & 6 deletions test/integration/tags/render_tag_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,12 @@ def test_recursively_rendered_template_does_not_produce_endless_loop
end
end

def test_includes_and_renders_count_towards_the_same_recursion_limit
def test_sub_contexts_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_include" %}',
'loop_include' => '{% include "loop_render" %}'
'loop_render' => '{% render "loop_render" %}',
)

assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_include" %}').render!
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_render" %}').render!
end
end

Expand Down Expand Up @@ -148,4 +146,23 @@ def test_decrement_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}'
end

def test_includes_will_not_render_inside_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'test_include' => '{% include "foo" %}'
)

assert_template_result 'include usage is not allowed in this context', '{% render "test_include" %}'
end

def test_includes_will_not_render_inside_nested_sibling_tags
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}'
)

assert_template_result 'include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}'
end
end
36 changes: 36 additions & 0 deletions test/unit/registers/disabled_tags_unit_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'test_helper'

class DisabledTagsUnitTest < Minitest::Test
include Liquid

def test_disables_tag_specified
register = DisabledTags.new
register.disable(%w(foo bar)) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
assert_equal false, register.disabled?("unknown")
end
end

def test_disables_nested_tags
register = DisabledTags.new
register.disable(["foo"]) do
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
register.disable(["bar"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
end
end
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
end
end

0 comments on commit 0db9c56

Please sign in to comment.