169 lines
4.4 KiB
Ruby
169 lines
4.4 KiB
Ruby
module Jekyll
|
|
module Tags
|
|
class IncludeAbsoluteTagError < StandardError
|
|
attr_accessor :path
|
|
|
|
def initialize(msg, path)
|
|
super(msg)
|
|
@path = path
|
|
end
|
|
end
|
|
|
|
class IncludeAbsoluteTag < Liquid::Tag
|
|
VALID_SYNTAX = %r!
|
|
([\w-]+)\s*=\s*
|
|
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))
|
|
!x
|
|
VARIABLE_SYNTAX = %r!
|
|
(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)
|
|
(?<params>.*)
|
|
!mx
|
|
|
|
FULL_VALID_SYNTAX = %r!\A\s*(?:#{VALID_SYNTAX}(?=\s|\z)\s*)*\z!
|
|
VALID_FILENAME_CHARS = %r!^[\w/\.-]+$!
|
|
|
|
def initialize(tag_name, markup, tokens)
|
|
super
|
|
matched = markup.strip.match(VARIABLE_SYNTAX)
|
|
if matched
|
|
@file = matched["variable"].strip
|
|
@params = matched["params"].strip
|
|
else
|
|
@file, @params = markup.strip.split(%r!\s+!, 2)
|
|
end
|
|
validate_params if @params
|
|
@tag_name = tag_name
|
|
end
|
|
|
|
def syntax_example
|
|
"{% #{@tag_name} 'file.ext' param='value' param2='value' %}"
|
|
end
|
|
|
|
def parse_params(context)
|
|
params = {}
|
|
markup = @params
|
|
|
|
while (match = VALID_SYNTAX.match(markup))
|
|
markup = markup[match.end(0)..-1]
|
|
|
|
value = if match[2]
|
|
match[2].gsub(%r!\\"!, '"')
|
|
elsif match[3]
|
|
match[3].gsub(%r!\\'!, "'")
|
|
elsif match[4]
|
|
context[match[4]]
|
|
end
|
|
|
|
params[match[1]] = value
|
|
end
|
|
params
|
|
end
|
|
|
|
def validate_file_name(file)
|
|
if file !~ VALID_FILENAME_CHARS
|
|
raise ArgumentError, <<-MSG
|
|
Invalid syntax for include tag. File contains invalid characters or sequences:
|
|
|
|
#{file}
|
|
|
|
Valid syntax:
|
|
|
|
#{syntax_example}
|
|
|
|
MSG
|
|
end
|
|
end
|
|
|
|
def validate_params
|
|
unless @params =~ FULL_VALID_SYNTAX
|
|
raise ArgumentError, <<-MSG
|
|
Invalid syntax for include tag:
|
|
|
|
#{@params}
|
|
|
|
Valid syntax:
|
|
|
|
#{syntax_example}
|
|
|
|
MSG
|
|
end
|
|
end
|
|
|
|
# Grab file read opts in the context
|
|
def file_read_opts(context)
|
|
context.registers[:site].file_read_opts
|
|
end
|
|
|
|
# Render the variable if required
|
|
def render_variable(context)
|
|
if @file =~ VARIABLE_SYNTAX
|
|
partial = context.registers[:site]
|
|
.liquid_renderer
|
|
.file("(variable)")
|
|
.parse(@file)
|
|
partial.render!(context)
|
|
end
|
|
end
|
|
|
|
def render(context)
|
|
site = context.registers[:site]
|
|
|
|
file = render_variable(context) || @file
|
|
# strip leading and trailing quote's
|
|
file = file.gsub!(/\A'|'\Z/, '')
|
|
validate_file_name(file)
|
|
|
|
source = File.expand_path(context.registers[:site].config['source']).freeze
|
|
path = File.join(source, file)
|
|
return unless path
|
|
|
|
partial = Liquid::Template.parse(read_file(path, context))
|
|
|
|
context.stack do
|
|
context["include"] = parse_params(context) if @params
|
|
begin
|
|
partial.render!(context)
|
|
rescue Liquid::Error => e
|
|
e.template_name = path
|
|
e.markup_context = "included " if e.markup_context.nil?
|
|
raise e
|
|
end
|
|
end
|
|
end
|
|
|
|
def valid_include_file?(path, dir, safe)
|
|
!outside_site_source?(path, dir, safe) && File.file?(path)
|
|
end
|
|
|
|
def outside_site_source?(path, dir, safe)
|
|
safe && !realpath_prefixed_with?(path, dir)
|
|
end
|
|
|
|
def realpath_prefixed_with?(path, dir)
|
|
File.exist?(path) && File.realpath(path).start_with?(dir)
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
# This method allows to modify the file content by inheriting from the class.
|
|
def read_file(file, context)
|
|
File.read(file, **file_read_opts(context))
|
|
end
|
|
|
|
private
|
|
|
|
def could_not_locate_message(file, includes_dirs, safe)
|
|
message = "Could not locate the included file '#{file}' in any of "\
|
|
"#{includes_dirs}. Ensure it exists in one of those directories and"
|
|
message + if safe
|
|
" is not a symlink as those are not allowed in safe mode."
|
|
else
|
|
", if it is a symlink, does not point outside your site source."
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
Liquid::Template.register_tag("include_absolute", Jekyll::Tags::IncludeAbsoluteTag)
|