Class: Sxn::Templates::TemplateProcessor

Inherits:
Object
  • Object
show all
Defined in:
lib/sxn/templates/template_processor.rb

Overview

TemplateProcessor provides secure, sandboxed template processing using Liquid. It ensures that templates cannot execute arbitrary code or access the filesystem.

Features:

  • Whitelisted variables only

  • No arbitrary code execution

  • Support for nested variable access (session.name, git.branch)

  • Built-in filters (upcase, downcase, join, etc.)

  • Template validation before processing

Example:

processor = TemplateProcessor.new
variables = { session: { name: "test" }, git: { branch: "main" } }
result = processor.process("Hello {{session.name}} on {{git.branch}}", variables)
# => "Hello test on main"

Constant Summary collapse

MAX_TEMPLATE_SIZE =

Maximum template size in bytes to prevent memory exhaustion

1_048_576
MAX_RENDER_TIME =

Maximum rendering time in seconds to prevent infinite loops

10
ALLOWED_FILTERS =

Allowed Liquid filters for security

%w[
  upcase downcase capitalize
  strip lstrip rstrip
  size length
  first last
  join split
  sort sort_natural reverse
  uniq compact
  date
  default
  escape escape_once
  truncate truncatewords
  replace replace_first
  remove remove_first
  plus minus times divided_by modulo
  abs ceil floor round
  at_least at_most
].freeze

Instance Method Summary collapse

Constructor Details

#initializeTemplateProcessor

Returns a new instance of TemplateProcessor.



50
51
52
# File 'lib/sxn/templates/template_processor.rb', line 50

def initialize
  create_secure_liquid_environment
end

Instance Method Details

#extract_variables(template_content) ⇒ Array<String>

Extract variables referenced in a template

Parameters:

  • template_content (String)

    The template content to analyze

Returns:

  • (Array<String>)

    List of variable names referenced in the template



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/sxn/templates/template_processor.rb', line 121

def extract_variables(template_content)
  variables = Set.new
  loop_variables = Set.new

  # Extract variables from {% if/unless variable %} expressions
  template_content.scan(/\{%\s*(?:if|unless)\s+(\w+)(?:\.\w+)*.*?%\}/) do |match|
    variables.add(match[0])
  end

  # Extract collection variables from {% for item in collection %} expressions
  template_content.scan(/\{%\s*for\s+(\w+)\s+in\s+(\w+)(?:\.\w+)*.*?%\}/) do |loop_var, collection_var|
    loop_variables.add(loop_var)
    variables.add(collection_var)
  end

  # Extract variables from {{ variable }} expressions, excluding loop variables
  # But only from outside control blocks
  content_outside_blocks = template_content.dup

  # Remove content inside control blocks to avoid extracting variables from inside conditionals
  content_outside_blocks.gsub!(/\{%\s*if\s+.*?\{%\s*endif\s*%\}/m, "")
  content_outside_blocks.gsub!(/\{%\s*unless\s+.*?\{%\s*endunless\s*%\}/m, "")
  content_outside_blocks.gsub!(/\{%\s*for\s+.*?\{%\s*endfor\s*%\}/m, "")

  content_outside_blocks.scan(/\{\{\s*(\w+)(?:\.\w+)*.*?\}\}/) do |match|
    var_name = match[0]
    variables.add(var_name) unless loop_variables.include?(var_name)
  end

  variables.to_a.sort
end

#process(template_content, variables = {}, options = {}) ⇒ String

Process a template string with the given variables

Parameters:

  • template_content (String)

    The template content to process

  • variables (Hash) (defaults to: {})

    Variables to make available in the template

  • options (Hash) (defaults to: {})

    Processing options

Options Hash (options):

  • :strict (Boolean) — default: true

    Whether to raise on undefined variables

  • :validate (Boolean) — default: true

    Whether to validate template syntax first

Returns:

  • (String)

    The processed template

Raises:

  • (TemplateTooLargeError)

    if template exceeds size limit

  • (TemplateTimeoutError)

    if processing takes too long

  • (TemplateSecurityError)

    if template contains disallowed content

  • (TemplateSyntaxError)

    if template has invalid syntax



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/sxn/templates/template_processor.rb', line 66

def process(template_content, variables = {}, options = {})
  options = { strict: true, validate: true }.merge(options)

  validate_template_size!(template_content)

  # Sanitize and whitelist variables
  sanitized_variables = sanitize_variables(variables)

  # Parse template with syntax validation
  template = parse_template(template_content, validate: options[:validate])

  # Render with timeout protection
  render_with_timeout(template, sanitized_variables, options)
rescue Liquid::SyntaxError => e
  raise Errors::TemplateSyntaxError, "Template syntax error: #{e.message}"
rescue Errors::TemplateTooLargeError, Errors::TemplateTimeoutError, Errors::TemplateRenderError => e
  # Re-raise specific template errors as-is
  raise e
rescue StandardError => e
  raise Errors::TemplateProcessingError, "Template processing failed: #{e.message}"
end

#process_file(template_path, variables = {}, options = {}) ⇒ String

Process a template file with the given variables

Parameters:

  • template_path (String, Pathname)

    Path to the template file

  • variables (Hash) (defaults to: {})

    Variables to make available in the template

  • options (Hash) (defaults to: {})

    Processing options (see #process)

Returns:

  • (String)

    The processed template

Raises:



95
96
97
98
99
100
101
102
# File 'lib/sxn/templates/template_processor.rb', line 95

def process_file(template_path, variables = {}, options = {})
  template_path = Pathname.new(template_path)

  raise Errors::TemplateNotFoundError, "Template file not found: #{template_path}" unless template_path.exist?

  template_content = template_path.read
  process(template_content, variables, options)
end

#validate_syntax(template_content) ⇒ Boolean Also known as: validate_template

Validate template syntax without processing

Parameters:

  • template_content (String)

    The template content to validate

Returns:

  • (Boolean)

    true if template is valid

Raises:

  • (TemplateSyntaxError)

    if template has invalid syntax



109
110
111
112
113
114
115
# File 'lib/sxn/templates/template_processor.rb', line 109

def validate_syntax(template_content)
  validate_template_size!(template_content)
  parse_template(template_content, validate: true)
  true
rescue Liquid::SyntaxError => e
  raise Errors::TemplateSyntaxError, "Template syntax error: #{e.message}"
end