limitations under the License.
]]--
+
+--- LuCI template library.
module("luci.template", package.seeall)
require("luci.config")
require("luci.util")
require("luci.fs")
+require("luci.sys")
require("luci.http")
-viewdir = luci.sys.libpath() .. "/view/"
+luci.config.template = luci.config.template or {}
+
+viewdir = luci.config.template.viewdir or luci.util.libpath() .. "/view"
+compiledir = luci.config.template.compiledir or luci.util.libpath() .. "/view"
-- Compile modes:
-- none: Never compile, only use precompiled data from files
-- memory: Always compile, do not save compiled files, ignore precompiled
-- file: Compile on demand, save compiled files, update precompiled
-compiler_mode = "memory"
-
-
--- This applies to compiler modes "always" and "smart"
---
--- Produce compiled lua code rather than lua sourcecode
--- WARNING: Increases template size heavily!!!
--- This produces the same bytecode as luac but does not have a strip option
-compiler_enable_bytecode = false
+compiler_mode = luci.config.template.compiler_mode or "memory"
-- Define the namespace for template modules
+context = luci.util.threadlocal()
+
viewns = {
- write = io.write,
- include = function(name) Template(name):render(getfenv(2)) end,
+ include = function(name) Template(name):render(getfenv(2)) end,
}
--- Compiles a given template into an executable Lua module
+--- Manually compile a given template into an executable Lua function
+-- @param template LuCI template
+-- @return Lua template function
function compile(template)
- -- Search all <% %> expressions (remember: Lua table indexes begin with #1)
- local function expr_add(command)
+ -- Search all <% %> expressions
+ local function expr_add(ws1, skip1, command, skip2, ws2)
table.insert(expr, command)
- return "<%" .. tostring(#expr) .. "%>"
+ return ( #skip1 > 0 and "" or ws1 ) ..
+ "<%" .. tostring(#expr) .. "%>" ..
+ ( #skip2 > 0 and "" or ws2 )
end
-- As "expr" should be local, we have to assign it to the "expr_add" scope
luci.util.extfenv(expr_add, "expr", expr)
-- Save all expressiosn to table "expr"
- template = template:gsub("<%%(.-)%%>", expr_add)
+ template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
local function sanitize(s)
- s = luci.util.escape(s)
- s = luci.util.escape(s, "'")
- s = luci.util.escape(s, "\n")
- return s
+ s = string.format("%q", s)
+ return s:sub(2, #s-1)
end
-- Escape and sanitize all the template (all non-expressions)
template = sanitize(template)
-- Template module header/footer declaration
- local header = "write('"
- local footer = "')"
+ local header = 'write("'
+ local footer = '")'
template = header .. template .. footer
-- Replacements
- local r_include = "')\ninclude('%s')\nwrite('"
- local r_i18n = "'..translate('%1','%2')..'"
- local r_pexec = "'..(%s or '')..'"
- local r_exec = "')\n%s\nwrite('"
+ local r_include = '")\ninclude("%s")\nwrite("'
+ local r_i18n = '"..translate("%1","%2").."'
+ local r_i18n2 = '"..translate("%1", "").."'
+ local r_pexec = '"..(%s or "").."'
+ local r_exec = '")\n%s\nwrite("'
-- Parse the expressions
for k,v in pairs(expr) do
local p = v:sub(1, 1)
+ v = v:gsub("%%", "%%%%")
local re = nil
if p == "+" then
re = r_include:format(sanitize(string.sub(v, 2)))
elseif p == ":" then
- re = sanitize(v):gsub(":(.-) (.+)", r_i18n)
+ if v:find(" ") then
+ re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
+ else
+ re = sanitize(v):gsub(":(.+)", r_i18n2)
+ end
elseif p == "=" then
re = r_pexec:format(v:sub(2))
+ elseif p == "#" then
+ re = ""
else
re = r_exec:format(v)
end
template = template:gsub("<%%"..tostring(k).."%%>", re)
end
- if compiler_enable_bytecode then
- tf = loadstring(template)
- template = string.dump(tf)
- end
-
- return template
+ return loadstring(template)
end
--- Oldstyle render shortcut
+--- Render a certain template.
+-- @param name Template name
+-- @param scope Scope to assign to template
function render(name, scope, ...)
scope = scope or getfenv(2)
- local s, t = pcall(Template, name)
+ local s, t = luci.util.copcall(Template, name)
if not s then
error(t)
else
-- Shared template cache to store templates in to avoid unnecessary reloading
Template.cache = {}
+setmetatable(Template.cache, {__mode = "v"})
-- Constructor - Reads and compiles the template on-demand
function Template.__init__(self, name)
- if self.cache[name] then
- self.template = self.cache[name]
- else
- self.template = nil
- end
+ self.template = self.cache[name]
-- Create a new namespace for this template
self.viewns = {}
-- Copy over from general namespace
- for k, v in pairs(viewns) do
- self.viewns[k] = v
- end
+ luci.util.update(self.viewns, viewns)
+ if context.viewns then
+ luci.util.update(self.viewns, context.viewns)
+ end
-- If we have a cached template, skip compiling and loading
if self.template then
return
end
+ -- Enforce cache security
+ local cdir = compiledir .. "/" .. luci.sys.process.info("uid")
+
-- Compile and build
- local sourcefile = viewdir .. name .. ".htm"
- local compiledfile = viewdir .. name .. ".lua"
+ local sourcefile = viewdir .. "/" .. name .. ".htm"
+ local compiledfile = cdir .. "/" .. luci.http.urlencode(name) .. ".lua"
local err
if compiler_mode == "file" then
local tplmt = luci.fs.mtime(sourcefile)
local commt = luci.fs.mtime(compiledfile)
+
+ if not luci.fs.mtime(cdir) then
+ luci.fs.mkdir(cdir, true)
+ luci.fs.chmod(luci.fs.dirname(cdir), "a+rxw")
+ end
-- Build if there is no compiled file or if compiled file is outdated
if ((commt == nil) and not (tplmt == nil))
source, err = luci.fs.readfile(sourcefile)
if source then
- local compiled = compile(source)
- luci.fs.writefile(compiledfile, compiled)
- self.template, err = loadstring(compiled)
+ local compiled, err = compile(source)
+
+ luci.fs.writefile(compiledfile, luci.util.get_bytecode(compiled))
+ self.template = compiled
end
else
self.template, err = loadfile(compiledfile)
local source
source, err = luci.fs.readfile(sourcefile)
if source then
- self.template, err = loadstring(compile(source))
+ self.template, err = compile(source)
end
end