mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 08:50:45 -07:00
e.g. sometimes you want to only accept strings | lookups.
{% render snippetName %} for example. snippetName is a string right now.
We don't want safe_parse_expression because this would allow snippetName
to be a number, a boolean, etc. But we still want to strict parse this.
So what we'll do is use parse_expression(string, safe: true), this is
an optional opt-in to say "I know what I'm doing". Usually that's
because you're using the output of Parser#something as the input of
parse_expression.
It is true that Parser#expression is subset of Expression.parse, it is
not true of the opposite (e.g. Expression.parse doesn't care about .5
and happily parses that as a global lookup of the variable named "5",
Parser#expression throws for that.)
diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb
index e5c321dc..9ab350f0 100644
--- a/lib/liquid/condition.rb
+++ b/lib/liquid/condition.rb
@@ -48,8 +48,8 @@ module Liquid
@@operators
end
- def self.parse_expression(parse_context, markup)
- @@method_literals[markup] || parse_context.parse_expression(markup)
+ def self.parse_expression(parse_context, markup, safe: false)
+ @@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
end
attr_reader :attachment, :child_condition
diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb
index 1c59fe4a..82cf5768 100644
--- a/lib/liquid/parse_context.rb
+++ b/lib/liquid/parse_context.rb
@@ -51,13 +51,13 @@ module Liquid
end
def safe_parse_expression(parser)
- Expression.safe_parse(parser)
+ Expression.safe_parse(parser, @string_scanner, @expression_cache)
end
- def parse_expression(markup)
+ def parse_expression(markup, safe: false)
# todo(guilherme): remove this once rigid mode is fully using safe_parse_expression
- # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if @error_mode == :rigid
- puts("🚨 parse_expression used in rigid mode") if @error_mode == :rigid
+ # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if !safe && @error_mode == :rigid
+ puts("🚨 parse_expression used in rigid mode") if !safe && @error_mode == :rigid
Expression.parse(markup, @string_scanner, @expression_cache)
end
diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb
index 656d2e47..374ee511 100644
--- a/lib/liquid/tag.rb
+++ b/lib/liquid/tag.rb
@@ -72,8 +72,8 @@ module Liquid
parse_context.safe_parse_expression(parser)
end
- def parse_expression(markup)
- parse_context.parse_expression(markup)
+ def parse_expression(markup, safe: false)
+ parse_context.parse_expression(markup, safe: safe)
end
end
end
diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb
index c2be5db1..3182983b 100644
--- a/lib/liquid/tags/for.rb
+++ b/lib/liquid/tags/for.rb
@@ -93,7 +93,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
- @collection_name = parse_expression(collection_name)
+ @collection_name = parse_expression(collection_name, safe: true)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb
index 342374f1..e25d6250 100644
--- a/lib/liquid/tags/if.rb
+++ b/lib/liquid/tags/if.rb
@@ -81,8 +81,8 @@ module Liquid
block.attach(new_body)
end
- def parse_expression(markup)
- Condition.parse_expression(parse_context, markup)
+ def parse_expression(markup, safe: false)
+ Condition.parse_expression(parse_context, markup, safe: safe)
end
def lax_parse(markup)
@@ -124,9 +124,9 @@ module Liquid
end
def parse_comparison(p)
- a = parse_expression(p.expression)
+ a = parse_expression(p.expression, safe: true)
if (op = p.consume?(:comparison))
- b = parse_expression(p.expression)
+ b = parse_expression(p.expression, safe: true)
Condition.new(a, op, b)
else
Condition.new(a)
diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb
index 6cdbfd6f..b72a235b 100644
--- a/lib/liquid/tags/include.rb
+++ b/lib/liquid/tags/include.rb
@@ -87,10 +87,11 @@ module Liquid
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
- template_name = p.expression
+ @template_name_expr = safe_parse_expression(p)
with_or_for = p.id?("for") || p.id?("with") || nil
+ @variable_name_expr = nil
if with_or_for
- variable_name = p.expression
+ @variable_name_expr = parse_expression(p.consume(:id), safe: true)
end
alias_name = nil
@@ -98,8 +99,6 @@ module Liquid
alias_name = p.consume(:id)
end
- @template_name_expr = parse_expression(template_name)
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@alias_name = alias_name
# optional comma
@@ -109,7 +108,7 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
- @attributes[key] = parse_expression(p.expression)
+ @attributes[key] = safe_parse_expression(p)
p.consume?(:comma) # optional comma
end
end
diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb
index 89c11063..4f716b24 100644
--- a/lib/liquid/tags/render.rb
+++ b/lib/liquid/tags/render.rb
@@ -88,10 +88,11 @@ module Liquid
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
- template_name = rigid_template_name(p)
+ @template_name_expr = parse_expression(rigid_template_name(p), safe: true)
+ @variable_name_expr = nil
with_or_for = p.id?("for") || p.id?("with") || nil
if with_or_for
- variable_name = p.expression
+ @variable_name_expr = safe_parse_expression(p)
end
alias_name = nil
@@ -99,8 +100,6 @@ module Liquid
alias_name = p.consume(:id)
end
- @template_name_expr = parse_expression(template_name)
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@alias_name = alias_name
@is_for_loop = (with_or_for == FOR)
@@ -111,7 +110,7 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
- @attributes[key] = parse_expression(p.expression)
+ @attributes[key] = safe_parse_expression(p)
p.consume?(:comma) # optional comma
end
end
diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb
index 20957065..a3623bc5 100644
--- a/lib/liquid/variable.rb
+++ b/lib/liquid/variable.rb
@@ -65,11 +65,11 @@ module Liquid
return if p.look(:end_of_string)
- @name = parse_context.parse_expression(p.expression)
+ @name = parse_context.safe_parse_expression(p)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
- @filters << parse_filter_expressions(filtername, filterargs)
+ @filters << parse_filter_expressions(filtername, filterargs, safe: true)
end
p.consume(:end_of_string)
end
@@ -122,15 +122,15 @@ module Liquid
private
- def parse_filter_expressions(filter_name, unparsed_args)
+ def parse_filter_expressions(filter_name, unparsed_args, safe: false)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
- if (matches = a.match(JustTagAttributes))
+ if (matches = a.match(JustTagAttributes)) # we'll need to fix this
keyword_args ||= {}
- keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
+ keyword_args[matches[1]] = parse_context.parse_expression(matches[2], safe: false)
else
- filter_args << parse_context.parse_expression(a)
+ filter_args << parse_context.parse_expression(a, safe: safe)
end
end
result = [filter_name, filter_args]
184 lines
4.6 KiB
Ruby
184 lines
4.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Liquid
|
|
# Container for liquid nodes which conveniently wraps decision making logic
|
|
#
|
|
# Example:
|
|
#
|
|
# c = Condition.new(1, '==', 1)
|
|
# c.evaluate #=> true
|
|
#
|
|
class Condition # :nodoc:
|
|
@@operators = {
|
|
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
|
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
|
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
|
'<' => :<,
|
|
'>' => :>,
|
|
'>=' => :>=,
|
|
'<=' => :<=,
|
|
'contains' => lambda do |_cond, left, right|
|
|
if left && right && left.respond_to?(:include?)
|
|
right = right.to_s if left.is_a?(String)
|
|
left.include?(right)
|
|
else
|
|
false
|
|
end
|
|
rescue Encoding::CompatibilityError
|
|
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
|
|
left.b.include?(right.b)
|
|
end,
|
|
}
|
|
|
|
class MethodLiteral
|
|
attr_reader :method_name, :to_s
|
|
|
|
def initialize(method_name, to_s)
|
|
@method_name = method_name
|
|
@to_s = to_s
|
|
end
|
|
end
|
|
|
|
@@method_literals = {
|
|
'blank' => MethodLiteral.new(:blank?, '').freeze,
|
|
'empty' => MethodLiteral.new(:empty?, '').freeze,
|
|
}
|
|
|
|
def self.operators
|
|
@@operators
|
|
end
|
|
|
|
def self.parse_expression(parse_context, markup, safe: false)
|
|
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
|
|
end
|
|
|
|
attr_reader :attachment, :child_condition
|
|
attr_accessor :left, :operator, :right
|
|
|
|
def initialize(left = nil, operator = nil, right = nil)
|
|
@left = left
|
|
@operator = operator
|
|
@right = right
|
|
|
|
@child_relation = nil
|
|
@child_condition = nil
|
|
end
|
|
|
|
def evaluate(context = deprecated_default_context)
|
|
condition = self
|
|
result = nil
|
|
loop do
|
|
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
|
|
|
case condition.child_relation
|
|
when :or
|
|
break if Liquid::Utils.to_liquid_value(result)
|
|
when :and
|
|
break unless Liquid::Utils.to_liquid_value(result)
|
|
else
|
|
break
|
|
end
|
|
condition = condition.child_condition
|
|
end
|
|
result
|
|
end
|
|
|
|
def or(condition)
|
|
@child_relation = :or
|
|
@child_condition = condition
|
|
end
|
|
|
|
def and(condition)
|
|
@child_relation = :and
|
|
@child_condition = condition
|
|
end
|
|
|
|
def attach(attachment)
|
|
@attachment = attachment
|
|
end
|
|
|
|
def else?
|
|
false
|
|
end
|
|
|
|
def inspect
|
|
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :child_relation
|
|
|
|
private
|
|
|
|
def equal_variables(left, right)
|
|
if left.is_a?(MethodLiteral)
|
|
if right.respond_to?(left.method_name)
|
|
return right.send(left.method_name)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
if right.is_a?(MethodLiteral)
|
|
if left.respond_to?(right.method_name)
|
|
return left.send(right.method_name)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
left == right
|
|
end
|
|
|
|
def interpret_condition(left, right, op, context)
|
|
# If the operator is empty this means that the decision statement is just
|
|
# a single variable. We can just poll this variable from the context and
|
|
# return this as the result.
|
|
return context.evaluate(left) if op.nil?
|
|
|
|
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
|
|
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
|
|
|
|
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
|
|
|
if operation.respond_to?(:call)
|
|
operation.call(self, left, right)
|
|
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
|
begin
|
|
left.send(operation, right)
|
|
rescue ::ArgumentError => e
|
|
raise Liquid::ArgumentError, e.message
|
|
end
|
|
end
|
|
end
|
|
|
|
def deprecated_default_context
|
|
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
|
" and will be removed from Liquid 6.0.0.")
|
|
Context.new
|
|
end
|
|
|
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
def children
|
|
[
|
|
@node.left,
|
|
@node.right,
|
|
@node.child_condition,
|
|
@node.attachment
|
|
].compact
|
|
end
|
|
end
|
|
end
|
|
|
|
class ElseCondition < Condition
|
|
def else?
|
|
true
|
|
end
|
|
|
|
def evaluate(_context)
|
|
true
|
|
end
|
|
end
|
|
end
|