Put template rendering in a jail

This commit is contained in:
Thomas Reynolds 2014-01-01 21:19:05 -08:00
parent 9798f152ca
commit 305d2f99ed
17 changed files with 233 additions and 169 deletions

View file

@ -10,6 +10,7 @@ matrix:
fast_finish: true
allow_failures:
- rvm: ruby-head
- rvm: 2.1.0
- rvm: jruby-19mode
env: TEST=true
before_install: git submodule update --init --recursive

View file

@ -44,15 +44,6 @@ module Middleman
# Runs after the build is finished
define_hook :after_build
# Mix-in helper methods. Accepts either a list of Modules
# and/or a block to be evaluated
# @return [void]
def self.helpers(*extensions, &block)
class_eval(&block) if block_given?
include(*extensions) if extensions.any?
end
delegate :helpers, :to => :"self.class"
# Root project directory (overwritten in middleman build/server)
# @return [String]
def self.root
@ -170,11 +161,16 @@ module Middleman
# Template cache
attr_reader :cache
attr_reader :generic_template_context
delegate :link_to, :image_tag, :to => :generic_template_context
# Initialize the Middleman project
def initialize(&block)
@cache = ::Tilt::Cache.new
@logger = ::Middleman::Logger.singleton
@config_context = ConfigContext.new(self)
@template_context_class = Class.new(Middleman::TemplateContext)
@generic_template_context = @template_context_class.new(self)
@config_context = ConfigContext.new(self, @template_context_class)
# Setup the default values from calls to set before initialization
self.class.config.load_settings(self.class.superclass.config.all_settings)

View file

@ -6,16 +6,32 @@ module Middleman
attr_reader :app
# Whitelist methods that can reach out.
delegate :config, :logger, :activate, :use, :map, :mime_type, :data, :helpers, :template_extensions, :root, :to => :app
delegate :config, :logger, :activate, :use, :map, :mime_type, :data, :template_extensions, :root, :to => :app
def initialize(app)
def initialize(app, template_context_class)
@app = app
@template_context_class = template_context_class
@ready_callbacks = []
@after_build_callbacks = []
@after_configuration_callbacks = []
@configure_callbacks = {}
end
def helpers(*helper_modules, &block)
helper_modules ||= []
if block_given?
block_module = Module.new
block_module.module_eval(&block)
helper_modules << block_module
end
helper_modules.each do |mod|
@template_context_class.send :include, mod
end
end
def ready(&block)
@ready_callbacks << block
end

View file

@ -182,6 +182,11 @@ module Middleman
end
if klass.is_a?(::Middleman::Extension)
# Forward Extension helpers to TemplateContext
(klass.class.defined_helpers || []).each do |m|
@template_context_class.send(:include, m)
end
::Middleman::Extension.activated_extension(klass)
end
end

View file

@ -24,7 +24,7 @@ module Middleman
require filename
next unless Object.const_defined?(module_name.to_sym)
helpers Object.const_get(module_name.to_sym)
@template_context_class.send :include, Object.const_get(module_name.to_sym)
end
end
end

View file

@ -81,13 +81,11 @@ module Middleman::CoreExtensions
end
end
helpers do
# Get the template data from a path
# @param [String] path
# @return [String]
def template_data_for_file(path)
extensions[:frontmatter].data(path).last
end
# Get the template data from a path
# @param [String] path
# @return [String]
def template_data_for_file(path)
data(path).last
end
def data(path)

View file

@ -1,13 +1,8 @@
# Shutup Tilt Warnings
# @private
class Tilt::Template
def warn(*args)
# Kernel.warn(*args)
end
end
require 'middleman-core/template_context'
# Rendering extension
module Middleman
module CoreExtensions
module Rendering
@ -136,16 +131,17 @@ module Middleman
::I18n.locale = opts[:lang] if opts[:lang]
end
# Use a dup of self as a context so that instance variables set within
# the template don't persist for other templates.
context = self.dup
# Sandboxed class for template eval
context = @template_context_class.new(self, locs, opts)
if context.respond_to?(:init_haml_helpers)
context.init_haml_helpers
end
blocks.each do |block|
context.instance_eval(&block)
end
# Store current locs/opts for later
@current_locs = locs, @current_opts = opts
# Keep rendering template until we've used up all extensions. This
# handles cases like `style.css.sass.erb`
content = nil
@ -170,60 +166,6 @@ module Middleman
# Pop all the saved variables from earlier as we may be returning to a
# previous render (layouts, partials, nested layouts).
::I18n.locale = old_locale if defined?(::I18n)
@content_blocks = nil
@current_locs = nil
@current_opts = nil
end
# Sinatra/Padrino compatible render method signature referenced by some view
# helpers. Especially partials.
#
# @param [String, Symbol] engine
# @param [String, Symbol] data
# @param [Hash] options
# @return [String]
def render(engine, data, options={}, &block)
data = data.to_s
locals = options[:locals]
found_partial = false
engine = nil
# If the path is known to the sitemap
if resource = sitemap.find_resource_by_path(current_path)
current_dir = File.dirname(resource.source_file)
engine = File.extname(resource.source_file)[1..-1].to_sym
# Look for partials relative to the current path
relative_dir = File.join(current_dir.sub(%r{^#{Regexp.escape(self.source_dir)}/?}, ''), data)
# Try to use the current engine first
found_partial, found_engine = resolve_template(relative_dir, :preferred_engine => engine, :try_without_underscore => true)
# Fall back to any engine available
if !found_partial
found_partial, found_engine = resolve_template(relative_dir, :try_without_underscore => true)
end
end
# Look in the partials_dir for the partial with the current engine
partials_path = File.join(config[:partials_dir], data)
if !found_partial && !engine.nil?
found_partial, found_engine = resolve_template(partials_path, :preferred_engine => engine, :try_without_underscore => true)
end
# Look in the root with any engine
if !found_partial
found_partial, found_engine = resolve_template(partials_path, :try_without_underscore => true)
end
# Render the partial if found, otherwide throw exception
if found_partial
render_individual_file(found_partial, locals, options, self, &block)
else
raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}"
end
end
# Render an on-disk file. Used for everything, including layouts.
@ -233,7 +175,7 @@ module Middleman
# @param [Hash] opts
# @param [Class] context
# @return [String]
def render_individual_file(path, locs = {}, opts = {}, context = self, &block)
def render_individual_file(path, locs = {}, opts = {}, context, &block)
path = path.to_s
# Detect the remdering engine from the extension
@ -244,7 +186,7 @@ module Middleman
context.current_engine, engine_was = engine, context.current_engine
# Save current buffer for later
@_out_buf, _buf_was = '', @_out_buf
_buf_was = context.save_buffer
# Read from disk or cache the contents of the file
body = if opts[:template_body]
@ -287,7 +229,7 @@ module Middleman
output
ensure
# Reset stored buffer
@_out_buf = _buf_was
context.restore_buffer(_buf_was)
context.current_engine = engine_was
end
@ -295,7 +237,11 @@ module Middleman
# @param [String] path
# @return [String]
def template_data_for_file(path)
File.read(File.expand_path(path, source_dir))
if extensions[:frontmatter]
extensions[:frontmatter].template_data_for_file(path)
else
File.read(File.expand_path(path, source_dir))
end
end
# Get a hash of configuration options for a given file extension, from
@ -393,49 +339,6 @@ module Middleman
layout_path
end
# Allow layouts to be wrapped in the contents of other layouts
# @param [String, Symbol] layout_name
# @return [void]
def wrap_layout(layout_name, &block)
# Save current buffer for later
@_out_buf, _buf_was = '', @_out_buf
layout_path = locate_layout(layout_name, self.current_engine)
extension = File.extname(layout_path)
engine = extension[1..-1].to_sym
# Store last engine for later (could be inside nested renders)
self.current_engine, engine_was = engine, self.current_engine
begin
content = if block_given?
capture_html(&block)
else
''
end
ensure
# Reset stored buffer
@_out_buf = _buf_was
end
concat_safe_content render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content }
ensure
self.current_engine = engine_was
end
# The currently rendering engine
# @return [Symbol, nil]
def current_engine
@_current_engine ||= nil
end
# The currently rendering engine
# @return [Symbol, nil]
def current_engine=(v)
@_current_engine = v
end
# Find a template on disk given a output path
# @param [String] request_path
# @param [Hash] options

View file

@ -20,7 +20,7 @@ module Middleman
def helpers(*m, &block)
self.defined_helpers ||= []
if block
if block_given?
mod = Module.new
mod.module_eval(&block)
m = [mod]
@ -81,8 +81,11 @@ module Middleman
def app=(app)
@app = app
(self.class.defined_helpers || []).each do |m|
app.class.send(:include, m)
ext = self
if ext.respond_to?(:instance_available)
@klass.instance_available do
ext.instance_available
end
end
end

View file

@ -4,24 +4,42 @@ require 'haml'
module Middleman
module Renderers
# Haml precompiles filters before the scope is even available,
# thus making it impossible to pass our Middleman instance
# in. So we have to resort to heavy hackery :(
class HamlTemplate < ::Tilt::HamlTemplate
def prepare
end
def evaluate(scope, locals, &block)
::Middleman::Renderers::Haml.last_haml_scope = scope
options = @options.merge(:filename => eval_file, :line => line)
@engine = ::Haml::Engine.new(data, options)
output = @engine.render(scope, locals, &block)
::Middleman::Renderers::Haml.last_haml_scope = nil
output
end
end
# Haml Renderer
module Haml
mattr_accessor :last_haml_scope
# Setup extension
class << self
# Once registered
def registered(app)
::Tilt.prefer(::Middleman::Renderers::HamlTemplate, 'haml')
app.before_configuration do
template_extensions :haml => :html
end
# Add haml helpers to context
app.send :include, ::Haml::Helpers
# Setup haml helper paths
app.ready do
init_haml_helpers
end
::Middleman::TemplateContext.send :include, ::Haml::Helpers
end
alias :included :registered
end

View file

@ -7,6 +7,8 @@ module Middleman
class KramdownTemplate < ::Tilt::KramdownTemplate
def evaluate(scope, locals, &block)
@output ||= begin
MiddlemanKramdownHTML.scope = ::Middleman::Renderers::Haml.last_haml_scope || scope
output, warnings = MiddlemanKramdownHTML.convert(@engine.root, @engine.options)
@engine.warnings.concat(warnings)
output
@ -16,13 +18,13 @@ module Middleman
# Custom Kramdown renderer that uses our helpers for images and links
class MiddlemanKramdownHTML < ::Kramdown::Converter::Html
cattr_accessor :middleman_app
cattr_accessor :scope
def convert_img(el, indent)
attrs = el.attr.dup
link = attrs.delete('src')
middleman_app.image_tag(link, attrs)
scope.image_tag(link, attrs)
end
def convert_a(el, indent)
@ -37,7 +39,7 @@ module Middleman
attr = el.attr.dup
link = attr.delete('href')
middleman_app.link_to(content, link, attr)
scope.link_to(content, link, attr)
end
end
end

View file

@ -30,11 +30,9 @@ module Middleman
if config[:markdown_engine] == :redcarpet
require 'middleman-core/renderers/redcarpet'
::Tilt.prefer(::Middleman::Renderers::RedcarpetTemplate, *markdown_exts)
MiddlemanRedcarpetHTML.middleman_app = self
elsif config[:markdown_engine] == :kramdown
require 'middleman-core/renderers/kramdown'
::Tilt.prefer(::Middleman::Renderers::KramdownTemplate, *markdown_exts)
MiddlemanKramdownHTML.middleman_app = self
elsif !config[:markdown_engine].nil?
# Map symbols to classes
markdown_engine_klass = if config[:markdown_engine].is_a? Symbol

View file

@ -40,6 +40,14 @@ module Middleman
renderer.new(render_options)
end
def evaluate(scope, locals, &block)
@output ||= begin
MiddlemanRedcarpetHTML.scope = ::Middleman::Renderers::Haml.last_haml_scope || scope
@engine.render(data)
end
end
private
def covert_options_to_aliases!
@ -51,7 +59,7 @@ module Middleman
# Custom Redcarpet renderer that uses our helpers for images and links
class MiddlemanRedcarpetHTML < ::Redcarpet::Render::HTML
cattr_accessor :middleman_app
cattr_accessor :scope
def initialize(options={})
@local_options = options.dup
@ -61,7 +69,7 @@ module Middleman
def image(link, title, alt_text)
if !@local_options[:no_images]
middleman_app.image_tag(link, :title => title, :alt => alt_text)
scope.image_tag(link, :title => title, :alt => alt_text)
else
link_string = link.dup
link_string << %Q{"#{title}"} if title && title.length > 0 && title != alt_text
@ -74,7 +82,7 @@ module Middleman
attributes = { :title => title }
attributes.merge!( @local_options[:link_attributes] ) if @local_options[:link_attributes]
middleman_app.link_to(content, link, attributes )
scope.link_to(content, link, attributes )
else
link_string = link.dup
link_string << %Q{"#{title}"} if title && title.length > 0 && title != alt_text

View file

@ -74,7 +74,7 @@ module Middleman
def sass_options
more_opts = { :filename => eval_file, :line => line, :syntax => syntax }
if @context.is_a?(::Middleman::Application) && file
if @context.is_a?(::Middleman::TemplateContext) && file
location_of_sass_file = @context.source_dir
parts = basename.split('.')

View file

@ -31,7 +31,7 @@ module Middleman
}, 'Callbacks that can exclude paths from the sitemap'
# Include instance methods
app.send :include, InstanceMethods
::Middleman::TemplateContext.send :include, InstanceMethods
end
end

View file

@ -0,0 +1,105 @@
module Middleman
class TemplateContext
attr_reader :app
attr_accessor :current_engine, :current_path
delegate :config, :logger, :sitemap, :build?, :development?, :data, :extensions, :source_dir, :root, :to => :app
def initialize(app, locs={}, opts={})
@app = app
@current_locs = locs
@current_opts = opts
end
def save_buffer
@_out_buf, _buf_was = '', @_out_buf
_buf_was
end
def restore_buffer(_buf_was)
@_out_buf = _buf_was
end
# Allow layouts to be wrapped in the contents of other layouts
# @param [String, Symbol] layout_name
# @return [void]
def wrap_layout(layout_name, &block)
# Save current buffer for later
_buf_was = save_buffer
layout_path = @app.locate_layout(layout_name, self.current_engine)
extension = File.extname(layout_path)
engine = extension[1..-1].to_sym
# Store last engine for later (could be inside nested renders)
self.current_engine, engine_was = engine, self.current_engine
begin
content = if block_given?
capture_html(&block)
else
''
end
ensure
# Reset stored buffer
restore_buffer(_buf_was)
end
concat_safe_content @app.render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content }
ensure
self.current_engine = engine_was
end
# Sinatra/Padrino compatible render method signature referenced by some view
# helpers. Especially partials.
#
# @param [String, Symbol] engine
# @param [String, Symbol] data
# @param [Hash] options
# @return [String]
def render(engine, data, options={}, &block)
data = data.to_s
locals = options[:locals]
found_partial = false
engine = nil
# If the path is known to the sitemap
if resource = sitemap.find_resource_by_path(current_path)
current_dir = File.dirname(resource.source_file)
engine = File.extname(resource.source_file)[1..-1].to_sym
# Look for partials relative to the current path
relative_dir = File.join(current_dir.sub(%r{^#{Regexp.escape(self.source_dir)}/?}, ''), data)
# Try to use the current engine first
found_partial, found_engine = @app.resolve_template(relative_dir, :preferred_engine => engine, :try_without_underscore => true)
# Fall back to any engine available
if !found_partial
found_partial, found_engine = @app.resolve_template(relative_dir, :try_without_underscore => true)
end
end
# Look in the partials_dir for the partial with the current engine
partials_path = File.join(config[:partials_dir], data)
if !found_partial && !engine.nil?
found_partial, found_engine = @app.resolve_template(partials_path, :preferred_engine => engine, :try_without_underscore => true)
end
# Look in the root with any engine
if !found_partial
found_partial, found_engine = @app.resolve_template(partials_path, :try_without_underscore => true)
end
# Render the partial if found, otherwide throw exception
if found_partial
@app.render_individual_file(found_partial, locals, options, self, &block)
else
raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}"
end
end
end
end

View file

@ -21,15 +21,15 @@ class Middleman::CoreExtensions::DefaultHelpers < ::Middleman::Extension
require 'active_support/core_ext/object/to_query'
app.helpers ::Padrino::Helpers::OutputHelpers
app.helpers ::Padrino::Helpers::TagHelpers
app.helpers ::Padrino::Helpers::AssetTagHelpers
app.helpers ::Padrino::Helpers::FormHelpers
app.helpers ::Padrino::Helpers::FormatHelpers
app.helpers ::Padrino::Helpers::RenderHelpers
app.helpers ::Padrino::Helpers::NumberHelpers
# app.helpers ::Padrino::Helpers::TranslationHelpers
app.helpers ::Padrino::Helpers::Breadcrumbs
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::OutputHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::TagHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::AssetTagHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::FormHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::FormatHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::RenderHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::NumberHelpers
# ::Middleman::TemplateContext.send :include, ::Padrino::Helpers::TranslationHelpers
::Middleman::TemplateContext.send :include, ::Padrino::Helpers::Breadcrumbs
app.config.define_setting :relative_links, false, 'Whether to generate relative links instead of absolute ones'
end
@ -39,6 +39,7 @@ class Middleman::CoreExtensions::DefaultHelpers < ::Middleman::Extension
# Make all block content html_safe
def content_tag(name, content = nil, options = nil, &block)
# safe_content_tag(name, content, options, &block)
if block_given?
options = content if content.is_a?(Hash)
content = capture_html(&block)
@ -61,17 +62,23 @@ class Middleman::CoreExtensions::DefaultHelpers < ::Middleman::Extension
def capture_html(*args, &block)
handler = auto_find_proper_handler(&block)
captured_block, captured_html = nil, ''
if handler && handler.is_type? && handler.block_is_type?(block)
if handler && handler.block_is_type?(block)
captured_html, captured_block = handler.capture_from_template(*args, &block)
end
# invoking the block directly if there was no template
captured_html = block_given? && ( captured_block || block.call(*args) ) if captured_html.blank?
captured_html = block_given? && ( captured_block || block.call(*args) ) if captured_html.blank?
captured_html
end
def auto_find_proper_handler(&block)
engine = block_given? ? File.extname(block.source_location[0])[1..-1].to_sym : current_engine
::Padrino::Helpers::OutputHelpers.handlers.map { |h| h.new(self) }.find { |h| h.engines.include?(engine) && h.is_type? }
if block_given?
engine = File.extname(block.source_location[0])[1..-1].to_sym
::Padrino::Helpers::OutputHelpers.handlers.map { |h| h.new(self) }.find { |h| h.engines.include?(engine) && h.block_is_type?(block) }
else
find_proper_handler
end
end
# Disable Padrino cache buster
@ -197,7 +204,7 @@ class Middleman::CoreExtensions::DefaultHelpers < ::Middleman::Extension
options_with_resource = options.dup
options_with_resource[:current_resource] ||= current_resource
::Middleman::Util.url_for(self, path_or_resource, options_with_resource)
::Middleman::Util.url_for(app, path_or_resource, options_with_resource)
end
# Overload the regular link_to to be sitemap-aware - if you

View file

@ -12,6 +12,10 @@
class Middleman::Extensions::Gzip < ::Middleman::Extension
option :exts, %w(.js .css .html .htm), 'File extensions to Gzip when building.'
class NumberHelpers
include ::Padrino::Helpers::NumberHelpers
end
def initialize(app, options_hash={})
super
@ -57,11 +61,11 @@ class Middleman::Extensions::Gzip < ::Middleman::Extension
if output_filename
total_savings += (old_size - new_size)
size_change_word = (old_size - new_size) > 0 ? 'smaller' : 'larger'
builder.say_status :gzip, "#{output_filename} (#{app.number_to_human_size((old_size - new_size).abs)} #{size_change_word})"
builder.say_status :gzip, "#{output_filename} (#{NumberHelpers.new.number_to_human_size((old_size - new_size).abs)} #{size_change_word})"
end
end
builder.say_status :gzip, "Total gzip savings: #{app.number_to_human_size(total_savings)}", :blue
builder.say_status :gzip, "Total gzip savings: #{NumberHelpers.new.number_to_human_size(total_savings)}", :blue
I18n.locale = old_locale
end