mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-17 01:40:42 -07:00
Yield to caller for external tags and filters
Instead of trying to handle external tags/filters inside the sandbox,
yield back to the caller with [:tag, ...] or [:filter, ...] args.
This cleanly separates concerns:
- Sandbox handles compiled template logic
- Caller handles external calls with full Ruby access
API:
compiled.render(assigns) do |call_type, *args|
case call_type
when :tag
tag_var, tag_assigns = args
# Handle with full Liquid context
when :filter
filter_name, input, filter_args = args
# Handle with custom filter handler
end
end
If no block is given, a default handler is used that:
- Renders external tags using Liquid::Context
- Calls filter methods via filter_handler
Also:
- Keep public_send in sandbox (safe, only calls public methods)
- Load date/time libs into sandbox for date filter support
- Preserve Date, DateTime, Time constants after lock
This commit is contained in:
+14
-1
@@ -125,6 +125,8 @@ module Liquid
|
||||
@box.require('base64')
|
||||
@box.require('bigdecimal')
|
||||
@box.require('bigdecimal/util') # For String#to_d etc.
|
||||
@box.require('date') # For date filter
|
||||
@box.require('time') # For Time.parse
|
||||
|
||||
# Now load the runtime which captures method references from these
|
||||
@box.require(RUNTIME_PATH)
|
||||
@@ -134,6 +136,8 @@ module Liquid
|
||||
require 'base64'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require RUNTIME_PATH
|
||||
end
|
||||
|
||||
@@ -143,6 +147,10 @@ module Liquid
|
||||
@user_constants << "CGI"
|
||||
@user_constants << "Base64"
|
||||
@user_constants << "BigDecimal"
|
||||
@user_constants << "Date"
|
||||
@user_constants << "DateTime"
|
||||
@user_constants << "Time"
|
||||
@user_constants << "Liquid" # For Liquid::Compile::CompiledContext
|
||||
end
|
||||
|
||||
# Add gem paths to the box's load_path so require works for gems
|
||||
@@ -374,22 +382,27 @@ module Liquid
|
||||
end
|
||||
|
||||
def neuter_basic_object!
|
||||
# Suppress the "__send__" warning - we know what we're doing
|
||||
@box.eval(<<~'RUBY')
|
||||
original_verbose = $VERBOSE
|
||||
$VERBOSE = nil
|
||||
class BasicObject
|
||||
undef_method(:instance_eval) rescue nil
|
||||
undef_method(:instance_exec) rescue nil
|
||||
undef_method(:__send__) rescue nil
|
||||
end
|
||||
$VERBOSE = original_verbose
|
||||
RUBY
|
||||
end
|
||||
|
||||
def neuter_object!
|
||||
@box.eval(<<~'RUBY')
|
||||
class Object
|
||||
# Keep public_send - it's safe (only calls public methods) and useful
|
||||
[:gem, :gem_original_require, :require, :require_relative, :load,
|
||||
:display, :define_singleton_method,
|
||||
:instance_variable_set, :remove_instance_variable,
|
||||
:extend, :send, :public_send,
|
||||
:extend, :send,
|
||||
].each { |m| undef_method(m) rescue nil }
|
||||
end
|
||||
RUBY
|
||||
|
||||
@@ -23,6 +23,22 @@ module Liquid
|
||||
# # Check security status
|
||||
# compiled.secure? # => true on Ruby 4.0+, false otherwise
|
||||
#
|
||||
# == External Calls (Tags and Filters)
|
||||
#
|
||||
# When the sandbox encounters an external tag or filter it can't handle,
|
||||
# it yields back to the caller. You can provide a block to handle these:
|
||||
#
|
||||
# compiled.render(assigns) do |call_type, *args|
|
||||
# case call_type
|
||||
# when :tag
|
||||
# tag_name, tag_obj, tag_context = args
|
||||
# tag_obj.render(tag_context)
|
||||
# when :filter
|
||||
# filter_name, input, filter_args = args
|
||||
# my_filter_handler.send(filter_name, input, *filter_args)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
class CompiledTemplate
|
||||
attr_reader :source, :external_tags
|
||||
attr_accessor :filter_handler
|
||||
@@ -60,21 +76,31 @@ module Liquid
|
||||
# execution happens in a secure sandbox. On earlier versions, a warning
|
||||
# is printed to STDERR.
|
||||
#
|
||||
# When the template needs to call an external tag or filter, it yields
|
||||
# back to the caller with [:tag, ...] or [:filter, ...] args. If no block
|
||||
# is given, a default handler is used.
|
||||
#
|
||||
# @param assigns [Hash] Variables to make available in the template
|
||||
# @param registers [Hash] Registers for custom tags (accessible via context.registers)
|
||||
# @param filter_handler [Object] Optional filter handler module
|
||||
# @param strict_variables [Boolean] Raise on undefined variables
|
||||
# @param strict_filters [Boolean] Raise on undefined filters
|
||||
# @yield [call_type, *args] Called for external tags/filters
|
||||
# @return [String] The rendered output
|
||||
#
|
||||
# @example Basic usage
|
||||
# compiled.render({ "name" => "World" })
|
||||
#
|
||||
# @example With registers
|
||||
# compiled.render({ "product" => product }, registers: { shop: current_shop })
|
||||
# @example With block for external calls
|
||||
# compiled.render(assigns) do |type, *args|
|
||||
# case type
|
||||
# when :tag then handle_tag(*args)
|
||||
# when :filter then handle_filter(*args)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def render(assigns = {}, registers: {}, filter_handler: nil, strict_variables: false, strict_filters: false)
|
||||
proc = to_proc
|
||||
def render(assigns = {}, registers: {}, filter_handler: nil, strict_variables: false, strict_filters: false, &block)
|
||||
compiled_proc = to_proc
|
||||
handler = filter_handler || @filter_handler
|
||||
|
||||
# Create a context for Drop support
|
||||
@@ -85,13 +111,11 @@ module Liquid
|
||||
strict_filters: strict_filters
|
||||
)
|
||||
|
||||
# Build arguments based on what the lambda expects
|
||||
args = [assigns]
|
||||
args << @external_tags if has_external_tags?
|
||||
args << handler if has_external_filters?
|
||||
args << context # Always pass context as last arg
|
||||
# Create the external call handler
|
||||
external_handler = block || default_external_handler(handler)
|
||||
|
||||
proc.call(*args)
|
||||
# Build arguments: assigns, context, external_handler
|
||||
compiled_proc.call(assigns, context, external_handler)
|
||||
end
|
||||
|
||||
# Alias for backwards compatibility
|
||||
@@ -119,6 +143,42 @@ module Liquid
|
||||
|
||||
private
|
||||
|
||||
# Default handler for external calls when no block is provided
|
||||
def default_external_handler(filter_handler)
|
||||
external_tags = @external_tags
|
||||
|
||||
->(call_type, *args) do
|
||||
case call_type
|
||||
when :tag
|
||||
tag_var, tag_assigns = args
|
||||
tag = external_tags[tag_var]
|
||||
return '' unless tag
|
||||
|
||||
# Create a context and render the tag
|
||||
ctx = Liquid::Context.new(
|
||||
[tag_assigns], {}, {},
|
||||
false, nil, {},
|
||||
Liquid::Environment.default
|
||||
)
|
||||
output = +''
|
||||
tag.render_to_output_buffer(ctx, output)
|
||||
output
|
||||
|
||||
when :filter
|
||||
filter_name, input, filter_args = args
|
||||
if filter_handler&.respond_to?(filter_name)
|
||||
m = filter_handler.method(filter_name)
|
||||
m.call(input, *filter_args)
|
||||
else
|
||||
input # Return unchanged if filter not found
|
||||
end
|
||||
|
||||
else
|
||||
raise ArgumentError, "Unknown external call type: #{call_type}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def compile_to_proc
|
||||
if Liquid::Box.secure?
|
||||
compile_in_sandbox
|
||||
@@ -142,8 +202,8 @@ module Liquid
|
||||
class #{template_class_name}
|
||||
TEMPLATE_PROC = #{@source}
|
||||
|
||||
def self.render(*args)
|
||||
TEMPLATE_PROC.call(*args)
|
||||
def self.render(*args, &block)
|
||||
TEMPLATE_PROC.call(*args, &block)
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
@@ -152,7 +212,9 @@ module Liquid
|
||||
template_class = @box[template_class_name]
|
||||
|
||||
# Return a proc that delegates to the sandboxed class
|
||||
->(assigns, *rest) { template_class.render(assigns, *rest) }
|
||||
->(assigns, context, external_handler) do
|
||||
template_class.render(assigns, context, external_handler)
|
||||
end
|
||||
end
|
||||
|
||||
# Compile without sandbox (Ruby < 4.0) - shows warning
|
||||
|
||||
@@ -203,7 +203,7 @@ module Liquid
|
||||
end
|
||||
|
||||
# Compile a filter that's not built-in
|
||||
# Uses __call_filter__ helper which must be provided by the runtime
|
||||
# Yields [:filter, name, input, args] to the external handler
|
||||
def self.compile_generic_filter(input, name, args, kwargs, compiler)
|
||||
# Mark that we're using external filters
|
||||
compiler.register_external_filter
|
||||
@@ -217,8 +217,8 @@ module Liquid
|
||||
|
||||
args_str = compiled_args.empty? ? "[]" : "[#{compiled_args.join(', ')}]"
|
||||
|
||||
# Call through the filter helper which delegates to registered filters
|
||||
"__call_filter__.call(#{name.inspect}, #{input}, #{args_str})"
|
||||
# Yield to the external handler
|
||||
"__external__.call(:filter, #{name.inspect}, #{input}, #{args_str})"
|
||||
end
|
||||
|
||||
# Compile a filter argument
|
||||
|
||||
@@ -203,39 +203,17 @@ module Liquid
|
||||
main_code = CodeGenerator.new
|
||||
compile_node(@template.root, main_code)
|
||||
|
||||
# Determine lambda parameters based on external dependencies
|
||||
params = ["assigns = {}"]
|
||||
params << "__external_tags__ = {}" unless @external_tags.empty?
|
||||
params << "__filter_handler__ = nil" if @has_external_filters
|
||||
params << "__context__ = nil"
|
||||
|
||||
code.line "->(#{params.join(', ')}) do"
|
||||
# Lambda signature: (assigns, context, external_handler)
|
||||
# - assigns: Hash of template variables
|
||||
# - context: CompiledContext for Drop support
|
||||
# - external_handler: Proc that handles [:tag, ...] and [:filter, ...] calls
|
||||
code.line "->(assigns, __context__, __external__) do"
|
||||
|
||||
code.indent do
|
||||
# Initialize the output buffer
|
||||
code.line '__output__ = +""'
|
||||
code.blank_line
|
||||
|
||||
# Create a compiled context if not provided (for Drop support)
|
||||
code.line "# Create context for Drop support"
|
||||
code.line "__context__ ||= Liquid::Compile::CompiledContext.new(assigns)"
|
||||
code.blank_line
|
||||
|
||||
# Note: All helper methods are provided by the LR module (pre-loaded runtime)
|
||||
# Templates use LR.to_s(), LR.lookup(), LR.output(), etc.
|
||||
|
||||
# Add external tag runtime helper if needed
|
||||
unless @external_tags.empty?
|
||||
compile_external_tag_helper(code)
|
||||
code.blank_line
|
||||
end
|
||||
|
||||
# Add external filter helper if needed
|
||||
if @has_external_filters
|
||||
compile_filter_helper(code)
|
||||
code.blank_line
|
||||
end
|
||||
|
||||
# Compile partial methods (before main body so they're available)
|
||||
compile_partials(code)
|
||||
|
||||
@@ -250,41 +228,6 @@ module Liquid
|
||||
code.to_s
|
||||
end
|
||||
|
||||
# Compile helper for calling external tags at runtime
|
||||
def compile_external_tag_helper(code)
|
||||
code.line "# Helper for calling external (unknown) tags at runtime"
|
||||
code.line "__call_external_tag__ = ->(tag_var, tag_assigns) {"
|
||||
code.indent do
|
||||
code.line "tag = __external_tags__[tag_var]"
|
||||
code.line "next '' unless tag"
|
||||
code.line "# Create a context using the default environment (which has filters registered)"
|
||||
code.line "ctx = Liquid::Context.new([tag_assigns], {}, {}, false, nil, {}, Liquid::Environment.default)"
|
||||
code.line "output = +''"
|
||||
code.line "# Use render_to_output_buffer to ensure block tags work correctly"
|
||||
code.line "tag.render_to_output_buffer(ctx, output)"
|
||||
code.line "output"
|
||||
end
|
||||
code.line "}"
|
||||
end
|
||||
|
||||
# Compile helper for calling external filters at runtime
|
||||
def compile_filter_helper(code)
|
||||
code.line "# Helper for calling external (unknown) filters at runtime"
|
||||
code.line "__call_filter__ = ->(name, input, args) {"
|
||||
code.indent do
|
||||
code.line "if __filter_handler__&.respond_to?(name)"
|
||||
code.indent do
|
||||
code.line "__filter_handler__.send(name, input, *args)"
|
||||
end
|
||||
code.line "else"
|
||||
code.indent do
|
||||
code.line "input # Return input unchanged if filter not found"
|
||||
end
|
||||
code.line "end"
|
||||
end
|
||||
code.line "}"
|
||||
end
|
||||
|
||||
# Compile all registered partials as inner methods
|
||||
def compile_partials(code)
|
||||
@partials.each do |name, method_name|
|
||||
@@ -387,10 +330,10 @@ module Liquid
|
||||
tag_var = register_external_tag(tag)
|
||||
tag_name = tag.class.name.split('::').last
|
||||
if debug?
|
||||
code.line "# External tag: #{tag_name} (delegated to runtime)"
|
||||
code.line "$stderr.puts '* WARN: Liquid external tag call - #{tag_name} (not compiled, delegated to runtime)' if $VERBOSE"
|
||||
code.line "# External tag: #{tag_name} (yields to caller)"
|
||||
end
|
||||
code.line "__output__ << __call_external_tag__.call(#{tag_var.inspect}, assigns)"
|
||||
# Yield [:tag, tag_var, assigns] to the external handler
|
||||
code.line "__output__ << __external__.call(:tag, #{tag_var.inspect}, assigns)"
|
||||
end
|
||||
|
||||
def find_tag_compiler(tag)
|
||||
|
||||
Reference in New Issue
Block a user