c8f4daa2dd54056da093340448f2799ae5b20c90
[project/luci.git] / libs / web / luasrc / template.lua
1 --[[
2 LuCI - Template Parser
3
4 Description:
5 A template parser supporting includes, translations, Lua code blocks
6 and more. It can be used either as a compiler or as an interpreter.
7
8 FileId: $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 local fs = require "luci.fs"
28 local sys = require "luci.sys"
29 local util = require "luci.util"
30 local table = require "table"
31 local string = require "string"
32 local config = require "luci.config"
33 local coroutine = require "coroutine"
34 local nixio = require "nixio", require "nixio.util"
35
36 local tostring, pairs, loadstring = tostring, pairs, loadstring
37 local setmetatable, loadfile = setmetatable, loadfile
38 local getfenv, setfenv, rawget = getfenv, setfenv, rawget
39 local assert, type, error = assert, type, error
40
41 --- LuCI template library.
42 module "luci.template"
43
44 config.template = config.template or {}
45
46 viewdir = config.template.viewdir or util.libpath() .. "/view"
47 compiledir = config.template.compiledir or util.libpath() .. "/view"
48
49
50 -- Compile modes:
51 -- memory: Always compile, do not save compiled files, ignore precompiled
52 -- file: Compile on demand, save compiled files, update precompiled
53 compiler_mode = config.template.compiler_mode or "memory"
54
55
56 -- Define the namespace for template modules
57 context = util.threadlocal()
58
59 --- Manually compile a given template into an executable Lua function
60 -- @param template LuCI template
61 -- @return Lua template function
62 function compile(template)
63 local expr = {}
64
65 -- Search all <% %> expressions
66 local function expr_add(ws1, skip1, command, skip2, ws2)
67 expr[#expr+1] = command
68 return ( #skip1 > 0 and "" or ws1 ) ..
69 "<%" .. tostring(#expr) .. "%>" ..
70 ( #skip2 > 0 and "" or ws2 )
71 end
72
73 -- Save all expressiosn to table "expr"
74 template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
75
76 local function sanitize(s)
77 s = "%q" % s
78 return s:sub(2, #s-1)
79 end
80
81 -- Escape and sanitize all the template (all non-expressions)
82 template = sanitize(template)
83
84 -- Template module header/footer declaration
85 local header = 'write("'
86 local footer = '")'
87
88 template = header .. template .. footer
89
90 -- Replacements
91 local r_include = '")\ninclude("%s")\nwrite("'
92 local r_i18n = '")\nwrite(translate("%1","%2"))\nwrite("'
93 local r_i18n2 = '")\nwrite(translate("%1", ""))\nwrite("'
94 local r_pexec = '")\nwrite(tostring(%s or ""))\nwrite("'
95 local r_exec = '")\n%s\nwrite("'
96
97 -- Parse the expressions
98 for k,v in pairs(expr) do
99 local p = v:sub(1, 1)
100 v = v:gsub("%%", "%%%%")
101 local re = nil
102 if p == "+" then
103 re = r_include:format(sanitize(string.sub(v, 2)))
104 elseif p == ":" then
105 if v:find(" ") then
106 re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
107 else
108 re = sanitize(v):gsub(":(.+)", r_i18n2)
109 end
110 elseif p == "=" then
111 re = r_pexec:format(v:sub(2))
112 elseif p == "#" then
113 re = ""
114 else
115 re = r_exec:format(v)
116 end
117 template = template:gsub("<%%"..tostring(k).."%%>", re)
118 end
119
120 return loadstring(template)
121 end
122
123 --- Render a certain template.
124 -- @param name Template name
125 -- @param scope Scope to assign to template (optional)
126 function render(name, scope)
127 return Template(name):render(scope or getfenv(2))
128 end
129
130
131 -- Template class
132 Template = util.class()
133
134 -- Shared template cache to store templates in to avoid unnecessary reloading
135 Template.cache = setmetatable({}, {__mode = "v"})
136
137
138 -- Constructor - Reads and compiles the template on-demand
139 function Template.__init__(self, name)
140 local function _encode_filename(str)
141
142 local function __chrenc( chr )
143 return "%%%02x" % string.byte( chr )
144 end
145
146 if type(str) == "string" then
147 str = str:gsub(
148 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
149 __chrenc
150 )
151 end
152
153 return str
154 end
155
156 self.template = self.cache[name]
157 self.name = name
158
159 -- Create a new namespace for this template
160 self.viewns = context.viewns
161
162 -- If we have a cached template, skip compiling and loading
163 if self.template then
164 return
165 end
166
167 -- Enforce cache security
168 local cdir = compiledir .. "/" .. sys.process.info("uid")
169
170 -- Compile and build
171 local sourcefile = viewdir .. "/" .. name
172 local compiledfile = cdir .. "/" .. _encode_filename(name) .. ".lua"
173 local err
174
175 if compiler_mode == "file" then
176 local tplmt = fs.mtime(sourcefile) or fs.mtime(sourcefile .. ".htm")
177 local commt = fs.mtime(compiledfile)
178
179 if not fs.mtime(cdir) then
180 fs.mkdir(cdir, true)
181 fs.chmod(fs.dirname(cdir), 777)
182 end
183
184 assert(tplmt or commt, "No such template: " .. name)
185
186 -- Build if there is no compiled file or if compiled file is outdated
187 if not commt or (commt and tplmt and commt < tplmt) then
188 local source
189 source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
190
191 if source then
192 local compiled, err = compile(source)
193
194 local f = nixio.open(compiledfile, "w", 600)
195 f:writeall(util.get_bytecode(compiled))
196 f:close()
197 self.template = compiled
198 end
199 else
200 assert(
201 sys.process.info("uid") == fs.stat(compiledfile, "uid")
202 and fs.stat(compiledfile, "modestr") == "rw-------",
203 "Fatal: Cachefile is not sane!"
204 )
205 self.template, err = loadfile(compiledfile)
206 end
207
208 elseif compiler_mode == "memory" then
209 local source
210 source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
211 if source then
212 self.template, err = compile(source)
213 end
214
215 end
216
217 -- If we have no valid template throw error, otherwise cache the template
218 if not self.template then
219 error(err)
220 else
221 self.cache[name] = self.template
222 end
223 end
224
225
226 -- Renders a template
227 function Template.render(self, scope)
228 scope = scope or getfenv(2)
229
230 -- Put our predefined objects in the scope of the template
231 setfenv(self.template, setmetatable({}, {__index =
232 function(tbl, key)
233 return rawget(tbl, key) or self.viewns[key] or scope[key]
234 end}))
235
236 -- Now finally render the thing
237 local stat, err = util.copcall(self.template)
238 if not stat then
239 error("Error in template %s: %s" % {self.name, err})
240 end
241 end