liquid-autoescape 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ require "liquid"
2
+
3
+ module Liquid
4
+ module Autoescape
5
+
6
+ # Liquid filters used to support the autoescape tag
7
+ module Filters
8
+
9
+ # Flag an input as exempt from autoescaping
10
+ #
11
+ # This is a non-transformative filter that works by registering itself
12
+ # in a variable's filter chain. If a variable detects this in its
13
+ # filters, no escaping will be performed on it.
14
+ #
15
+ # @param [String] input A variable's content
16
+ # @return [String] The unmodified content
17
+ def skip_escape(input)
18
+ input
19
+ end
20
+
21
+ end
22
+
23
+ Template.register_filter(Filters)
24
+
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ require "liquid"
2
+ require "liquid/autoescape"
3
+ require "liquid/autoescape/template_variable"
4
+
5
+ module Liquid
6
+ class Variable
7
+
8
+ # @private
9
+ alias_method :non_escaping_render, :render
10
+
11
+ # Possibly render the variable with HTML escaping applied
12
+ #
13
+ # If the autoescaping context variable has been set by the +autoescape+ tag
14
+ # or Liquid autoescaping is globally enabled, this will run the variable
15
+ # through the global exemption list to determine if it is exempt from
16
+ # autoescaping. If it is not, its contents will be rendered as a string
17
+ # with all unsafe HTML characters escaped. In all other cases, the
18
+ # original, unescaped value of the variable will be rendered.
19
+ #
20
+ # @param [Liquid::Context] context The variable's rendering context
21
+ # @return [String] The potentially escaped contents of the variable
22
+ def render(context)
23
+ if !Autoescape.configuration.global? && !context[Autoescape::ENABLED_FLAG]
24
+ return non_escaping_render(context)
25
+ end
26
+
27
+ variable = Autoescape::TemplateVariable.from_liquid_variable(self)
28
+ is_exempt = Autoescape.configuration.exemptions.apply?(variable)
29
+
30
+ @filters << [:escape, []] unless is_exempt
31
+ output = non_escaping_render(context)
32
+ @filters.pop unless is_exempt
33
+
34
+ output
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ require "liquid"
2
+ require "liquid/autoescape/liquid_ext/variable"
3
+
4
+ module Liquid
5
+ module Autoescape
6
+ module Tags
7
+
8
+ # A block tag that automatically escapes all variables contained within it
9
+ #
10
+ # All contained variables will have dangerous HTML characters escaped.
11
+ # Any variables that should be exempt from escaping should have the
12
+ # +skip_escape+ filter applied to them.
13
+ #
14
+ # @example
15
+ # {% assign untrusted = "<script>window.reload();</script>" %}
16
+ # {% assign trusted = "<strong>Text</strong>" %}
17
+ #
18
+ # {% autoescape %}
19
+ # {{ untrusted }}
20
+ # {{ trusted | skip_escape }}
21
+ # {% endautoescape %}
22
+ class Autoescape < Block
23
+
24
+ def initialize(tag_name, markup, tokens)
25
+ unless markup.empty?
26
+ raise SyntaxError, "Syntax Error in 'autoescape' - Valid syntax: {% autoescape %}"
27
+ end
28
+
29
+ super
30
+ end
31
+
32
+ def render(context)
33
+ context.stack do
34
+ context[ENABLED_FLAG] = true
35
+ super
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ Template.register_tag("autoescape", Autoescape)
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,74 @@
1
+ module Liquid
2
+ module Autoescape
3
+
4
+ # A wrapper around a Liquid variable used in a template
5
+ #
6
+ # This provides a consistent interface to a Liquid variable, accounting
7
+ # for structural differences in variables between different Liquid versions
8
+ # and exposing a simple list of applied filters. All exemptions are
9
+ # determined by examining instances of this object.
10
+ class TemplateVariable
11
+
12
+ # The name of the variable
13
+ #
14
+ # @return [String]
15
+ attr_reader :name
16
+
17
+ # The names of the filters applied to the variable
18
+ #
19
+ # @return [Array<Symbol>]
20
+ attr_reader :filters
21
+
22
+ class << self
23
+
24
+ # Create a wrapper around a Liquid variable instance
25
+ #
26
+ # This normalizes the variable's information, since Liquid handles
27
+ # variable names differently across versions.
28
+ #
29
+ # @param [Liquid::Variable] variable A Liquid variable as used in a template
30
+ # @return [Liquid::Autoescape::TemplateVariable]
31
+ def from_liquid_variable(variable)
32
+ name = normalize_variable_name(variable)
33
+ filters = variable.filters.map { |f| f.first.to_sym }
34
+
35
+ new(:name => name, :filters => filters)
36
+ end
37
+
38
+ private
39
+
40
+ # Normalize the name of a Liquid variable
41
+ #
42
+ # Liquid 2 exposes the full variable name directly on the
43
+ # +Liquid::Variable+ instance, while Liquid 3 manages it via a
44
+ # +Liquid::VariableLookup+ instance that tracks both the base name and
45
+ # any lookup paths involved.
46
+ #
47
+ # @param [Liquid::Variable] variable A Liquid variable as used in a template
48
+ # @return [String] The name of the Liquid variable
49
+ def normalize_variable_name(variable)
50
+ lookup_name = variable.name.instance_variable_get("@name")
51
+ return variable.name unless lookup_name
52
+
53
+ parts = [lookup_name]
54
+ variable.name.instance_variable_get("@lookups").each do |lookup|
55
+ parts << lookup
56
+ end
57
+ parts.join(".")
58
+ end
59
+
60
+ end
61
+
62
+ # Create a wrapper around a Liquid variable used in a template
63
+ #
64
+ # @options options [String] :name The name of the variable
65
+ # @options options [Array<Symbol>] :filters The filters applied to the variable
66
+ def initialize(options={})
67
+ @name = options.fetch(:name)
68
+ @filters = options[:filters] || []
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ module Liquid
2
+ module Autoescape
3
+ VERSION = "0.2.1"
4
+ end
5
+ end
@@ -0,0 +1,200 @@
1
+ require "liquid/autoescape"
2
+
3
+ describe "{% autoescape %}" do
4
+
5
+ def verify_template_output(template, expected, context={})
6
+ rendered = Liquid::Template.parse(template).render!(context)
7
+ expect(rendered).to eq(expected)
8
+ end
9
+
10
+ it "handles empty content" do
11
+ verify_template_output(
12
+ "{% autoescape %}{% endautoescape %}",
13
+ ""
14
+ )
15
+ end
16
+
17
+ it "handles non-variable content" do
18
+ verify_template_output(
19
+ "{% autoescape %}Static{% endautoescape %}",
20
+ "Static"
21
+ )
22
+ end
23
+
24
+ it "escapes all dangerous HTML characters" do
25
+ verify_template_output(
26
+ "{% autoescape %}{{ variable }}{% endautoescape %}",
27
+ "&lt;tag&gt; &amp; &quot;quote&quot;",
28
+ "variable" => '<tag> & "quote"'
29
+ )
30
+ end
31
+
32
+ it "applies HTML escaping to all variables inside the block tag" do
33
+ verify_template_output(
34
+ "{% autoescape %}{{ one }} {{ two }} {{ three }}{% endautoescape %}",
35
+ "&lt;tag&gt; &amp; &quot;quote&quot;",
36
+ "one" => "<tag>", "two" => "&", "three" => '"quote"'
37
+ )
38
+ end
39
+
40
+ it "applies HTML escaping to a filtered variable" do
41
+ verify_template_output(
42
+ "{% autoescape %}{{ filtered | downcase | capitalize }}{% endautoescape %}",
43
+ "A &lt;strong&gt; tag",
44
+ "filtered" => "A <STRONG> Tag"
45
+ )
46
+ end
47
+
48
+ it "does not double-escape variables" do
49
+ verify_template_output(
50
+ "{% autoescape %}{{ escaped | escape }}{% endautoescape %}",
51
+ "HTML &amp; CSS",
52
+ "escaped" => "HTML & CSS"
53
+ )
54
+ end
55
+
56
+ it "does not escape variables outside the block tag" do
57
+ verify_template_output(
58
+ "{{ variable }} {% autoescape %}{{ variable }}{% endautoescape %} {{ variable }}",
59
+ "& &amp; &",
60
+ "variable" => "&"
61
+ )
62
+ end
63
+
64
+ it "can be called multiple times" do
65
+ verify_template_output(
66
+ "{% autoescape %}{{ var }}{% endautoescape %} {{ var }} {% autoescape %}{{ var }}{% endautoescape %}",
67
+ "&amp; & &amp;",
68
+ "var" => "&"
69
+ )
70
+ end
71
+
72
+ it "does not escape variables with the skip_escape filter applied" do
73
+ verify_template_output(
74
+ "{% autoescape %}{{ variable | skip_escape }}{% endautoescape %}",
75
+ "<strong>&amp;</strong>",
76
+ "variable" => "<strong>&amp;</strong>"
77
+ )
78
+ end
79
+
80
+ it "raises an error when called with arguments" do
81
+ invalid = "{% autoescape on %}{% endautoescape %}"
82
+ expect { Liquid::Template.parse(invalid) }.to raise_error(Liquid::SyntaxError)
83
+ end
84
+
85
+ describe "configuration options" do
86
+
87
+ after(:each) { Liquid::Autoescape.reconfigure }
88
+
89
+ context "with global mode enabled" do
90
+
91
+ before(:each) do
92
+ Liquid::Autoescape.configure do |config|
93
+ config.global = true
94
+ end
95
+ end
96
+
97
+ it "escapes variables outside of an autoescape block" do
98
+ verify_template_output(
99
+ "{{ variable }}",
100
+ "&amp;",
101
+ "variable" => "&"
102
+ )
103
+ end
104
+
105
+ it "escapes variables in an autoescape block" do
106
+ verify_template_output(
107
+ "{% autoescape %}{{ variable }}{% endautoescape %}",
108
+ "&amp;",
109
+ "variable" => "&"
110
+ )
111
+ end
112
+
113
+ it "respects escaping filters" do
114
+ verify_template_output(
115
+ "{{ variable | skip_escape }}{% autoescape %}{{ variable | skip_escape }}{% endautoescape %}",
116
+ "&&",
117
+ "variable" => "&"
118
+ )
119
+ end
120
+
121
+ end
122
+
123
+ context "with custom exemptions" do
124
+
125
+ let(:exemptions) do
126
+ Module.new do
127
+ def exemption(variable)
128
+ variable.name == "module"
129
+ end
130
+ end
131
+ end
132
+
133
+ before(:each) do
134
+ Liquid::Autoescape.configure do |config|
135
+ config.exemptions.add { |variable| variable.name == "filter" }
136
+ config.exemptions.import(exemptions)
137
+ end
138
+ end
139
+
140
+ it "does not escape exempt variables" do
141
+ verify_template_output(
142
+ "{% autoescape %}{{ filter }} {{ module }} {{ other }}{% endautoescape %}",
143
+ "<a> <b> &lt;i&gt;",
144
+ "filter" => "<a>", "module" => "<b>", "other" => "<i>"
145
+ )
146
+ end
147
+
148
+ it "can handle exemptions with lookup-style variable names" do
149
+ Liquid::Autoescape.configure do |config|
150
+ config.exemptions.add { |variable| variable.name == "root.one" }
151
+ config.exemptions.add { |variable| variable.name == "trunk.branch.leaf" }
152
+ end
153
+
154
+ verify_template_output(
155
+ "{% autoescape %}{{ root.one }} {{ root.two }} {{ trunk.branch.leaf }}{% endautoescape %}",
156
+ "<a> &lt;b&gt; <i>",
157
+ "root" => {"one" => "<a>", "two" => "<b>"},
158
+ "trunk" => {"branch" => {"leaf" => "<i>"}}
159
+ )
160
+ end
161
+
162
+ it "respects the default exemptions" do
163
+ verify_template_output(
164
+ "{% autoescape %}{{ filter | skip_escape }} {{ other }}{% endautoescape %}",
165
+ "<a> &lt;b&gt;",
166
+ "filter" => "<a>", "other" => "<b>"
167
+ )
168
+ end
169
+
170
+ end
171
+
172
+ context "with trusted filters" do
173
+
174
+ before(:each) do
175
+ Liquid::Autoescape.configure do |config|
176
+ config.trusted_filters << :downcase
177
+ end
178
+ end
179
+
180
+ it "does not forget the default escaping filters" do
181
+ verify_template_output(
182
+ "{% autoescape %}{{ one | skip_escape }} {{ two | escape }}{% endautoescape %}",
183
+ "<a> &lt;b&gt;",
184
+ "one" => "<a>", "two" => "<b>"
185
+ )
186
+ end
187
+
188
+ it "exempts variables that use one of the trusted filters" do
189
+ verify_template_output(
190
+ "{% autoescape %}{{ variable | downcase }} {{ variable | capitalize }}{% endautoescape %}",
191
+ "r&d R&amp;d",
192
+ "variable" => "R&D"
193
+ )
194
+ end
195
+
196
+ end
197
+
198
+ end
199
+
200
+ end
@@ -0,0 +1,49 @@
1
+ require "liquid/autoescape"
2
+ require "liquid/autoescape/configuration"
3
+
4
+ module Liquid
5
+ describe Autoescape do
6
+
7
+ after(:each) { Autoescape.reconfigure }
8
+
9
+ describe ".configure" do
10
+
11
+ it "allows autoescape settings to be customized" do
12
+ Autoescape.configure do |config|
13
+ expect(config).to be_an_instance_of(Autoescape::Configuration)
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ describe ".reconfigure" do
20
+
21
+ it "undoes any user configuration" do
22
+ Autoescape.configure do |config|
23
+ config.trusted_filters << :my_custom_filter
24
+ end
25
+
26
+ expect(Autoescape.configuration.trusted_filters).to include(:my_custom_filter)
27
+
28
+ Autoescape.reconfigure
29
+ expect(Autoescape.configuration.trusted_filters).to_not include(:my_custom_filter)
30
+ end
31
+
32
+ end
33
+
34
+ describe ".configuration" do
35
+
36
+ it "exposes the current configuration object" do
37
+ Autoescape.configure do |config|
38
+ config.global = true
39
+ end
40
+
41
+ config = Autoescape.configuration
42
+ expect(config).to be_an_instance_of(Autoescape::Configuration)
43
+ expect(config.global?).to be(true)
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,72 @@
1
+ require "liquid/autoescape/configuration"
2
+
3
+ module Liquid
4
+ module Autoescape
5
+ describe Configuration do
6
+
7
+ let(:config) { Configuration.new }
8
+
9
+ describe "global mode" do
10
+
11
+ it "is off by default" do
12
+ expect(config.global?).to be(false)
13
+ end
14
+
15
+ it "can be turned on" do
16
+ config.global = true
17
+ expect(config.global?).to be(true)
18
+ end
19
+
20
+ end
21
+
22
+ describe "the list of custom exemptions" do
23
+
24
+ it "has default exemptions" do
25
+ expect(config.exemptions.populated?).to be(true)
26
+ end
27
+
28
+ it "can accept custom exemption filters" do
29
+ expect { config.exemptions.add { true } }.to change { config.exemptions.size }.by(1)
30
+ end
31
+
32
+ it "cannot directly exemption filters" do
33
+ exemption = lambda { true }
34
+ expect { config.exemptions << exemption }.to raise_error
35
+ end
36
+
37
+ end
38
+
39
+ describe "the list of trusted filters" do
40
+
41
+ it "is empty by default" do
42
+ expect(config.trusted_filters).to be_empty
43
+ end
44
+
45
+ it "can receive additional filters" do
46
+ config.trusted_filters << :downcase
47
+ expect(config.trusted_filters).to match_array([:downcase])
48
+ end
49
+
50
+ end
51
+
52
+ describe "#reset" do
53
+
54
+ it "restores the default configuration values" do
55
+ config.global = true
56
+ config.exemptions.add { true }
57
+ config.trusted_filters << :downcase
58
+
59
+ expect(config.global?).to be(true)
60
+ expect(config.trusted_filters.size).to be(1)
61
+
62
+ expect { config.reset }.to change { config.exemptions.size }.by(-1)
63
+
64
+ expect(config.global?).to be(false)
65
+ expect(config.trusted_filters).to be_empty
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
72
+ end