907403fcbba7d4ce79ff76475e3f5474017e571d
[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 --- LuCI template library.
28 module("luci.template", package.seeall)
29
30 require("luci.config")
31 require("luci.util")
32 require("luci.fs")
33 require("luci.sys")
34 require("luci.http")
35
36 luci.config.template = luci.config.template or {}
37
38 viewdir    = luci.config.template.viewdir or luci.util.libpath() .. "/view"
39 compiledir = luci.config.template.compiledir or luci.util.libpath() .. "/view"
40
41
42 -- Compile modes:
43 -- none:        Never compile, only use precompiled data from files
44 -- memory:      Always compile, do not save compiled files, ignore precompiled 
45 -- file:        Compile on demand, save compiled files, update precompiled
46 compiler_mode = luci.config.template.compiler_mode or "memory"
47
48
49 -- Define the namespace for template modules
50 context = luci.util.threadlocal()
51
52 viewns = {
53         include    = function(name) Template(name):render(getfenv(2)) end,
54 }
55
56 --- Manually  compile a given template into an executable Lua function
57 -- @param template      LuCI template
58 -- @return                      Lua template function
59 function compile(template)      
60         -- Search all <% %> expressions
61         local function expr_add(ws1, skip1, command, skip2, ws2)
62                 table.insert(expr, command)
63                 return ( #skip1 > 0 and "" or ws1 ) .. 
64                        "<%" .. tostring(#expr) .. "%>" ..
65                        ( #skip2 > 0 and "" or ws2 )
66         end
67         
68         -- As "expr" should be local, we have to assign it to the "expr_add" scope 
69         local expr = {}
70         luci.util.extfenv(expr_add, "expr", expr)
71         
72         -- Save all expressiosn to table "expr"
73         template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
74         
75         local function sanitize(s)
76                 s = string.format("%q", s)
77                 return s:sub(2, #s-1)
78         end
79         
80         -- Escape and sanitize all the template (all non-expressions)
81         template = sanitize(template)
82
83         -- Template module header/footer declaration
84         local header = 'write("'
85         local footer = '")'
86         
87         template = header .. template .. footer
88         
89         -- Replacements
90         local r_include = '")\ninclude("%s")\nwrite("'
91         local r_i18n    = '"..translate("%1","%2").."'
92         local r_i18n2    = '"..translate("%1", "").."'
93         local r_pexec   = '"..(%s or "").."'
94         local r_exec    = '")\n%s\nwrite("'
95         
96         -- Parse the expressions
97         for k,v in pairs(expr) do
98                 local p = v:sub(1, 1)
99                 v = v:gsub("%%", "%%%%")
100                 local re = nil
101                 if p == "+" then
102                         re = r_include:format(sanitize(string.sub(v, 2)))
103                 elseif p == ":" then
104                         if v:find(" ") then
105                                 re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
106                         else
107                                 re = sanitize(v):gsub(":(.+)", r_i18n2)
108                         end
109                 elseif p == "=" then
110                         re = r_pexec:format(v:sub(2))
111                 elseif p == "#" then
112                         re = ""
113                 else
114                         re = r_exec:format(v)
115                 end
116                 template = template:gsub("<%%"..tostring(k).."%%>", re)
117         end
118
119         return loadstring(template)
120 end
121
122 --- Render a certain template.
123 -- @param name          Template name
124 -- @param scope         Scope to assign to template
125 function render(name, scope, ...)
126         scope = scope or getfenv(2)
127         local s, t = luci.util.copcall(Template, name)
128         if not s then
129                 error(t)
130         else
131                 t:render(scope, ...)
132         end
133 end
134
135
136 -- Template class
137 Template = luci.util.class()
138
139 -- Shared template cache to store templates in to avoid unnecessary reloading
140 Template.cache = {}
141 setmetatable(Template.cache, {__mode = "v"})
142
143
144 -- Constructor - Reads and compiles the template on-demand
145 function Template.__init__(self, name)  
146         self.template = self.cache[name]
147         
148         -- Create a new namespace for this template
149         self.viewns = {}
150         
151         -- Copy over from general namespace
152         luci.util.update(self.viewns, viewns)
153         if context.viewns then
154                 luci.util.update(self.viewns, context.viewns)
155         end
156         
157         -- If we have a cached template, skip compiling and loading
158         if self.template then
159                 return
160         end
161         
162         -- Enforce cache security
163         local cdir = compiledir .. "/" .. luci.sys.process.info("uid")
164         
165         -- Compile and build
166         local sourcefile   = viewdir    .. "/" .. name .. ".htm"
167         local compiledfile = cdir .. "/" .. luci.http.urlencode(name) .. ".lua"
168         local err       
169         
170         if compiler_mode == "file" then
171                 local tplmt = luci.fs.mtime(sourcefile)
172                 local commt = luci.fs.mtime(compiledfile)
173                 
174                 if not luci.fs.mtime(cdir) then
175                         luci.fs.mkdir(cdir, true)
176                         luci.fs.chmod(luci.fs.dirname(cdir), "a+rxw")
177                 end
178                                 
179                 -- Build if there is no compiled file or if compiled file is outdated
180                 if ((commt == nil) and not (tplmt == nil))
181                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
182                         local source
183                         source, err = luci.fs.readfile(sourcefile)
184                         
185                         if source then
186                                 local compiled, err = compile(source)
187                                 
188                                 luci.fs.writefile(compiledfile, luci.util.get_bytecode(compiled))
189                                 self.template = compiled
190                         end
191                 else
192                         self.template, err = loadfile(compiledfile)
193                 end
194                 
195         elseif compiler_mode == "none" then
196                 self.template, err = loadfile(self.compiledfile)
197                 
198         elseif compiler_mode == "memory" then
199                 local source
200                 source, err = luci.fs.readfile(sourcefile)
201                 if source then
202                         self.template, err = compile(source)
203                 end
204                         
205         end
206         
207         -- If we have no valid template throw error, otherwise cache the template
208         if not self.template then
209                 error(err)
210         else
211                 self.cache[name] = self.template
212         end
213 end
214
215
216 -- Renders a template
217 function Template.render(self, scope)
218         scope = scope or getfenv(2)
219         
220         -- Save old environment
221         local oldfenv = getfenv(self.template)
222         
223         -- Put our predefined objects in the scope of the template
224         luci.util.resfenv(self.template)
225         luci.util.updfenv(self.template, scope)
226         luci.util.updfenv(self.template, self.viewns)
227         
228         -- Now finally render the thing
229         self.template()
230         
231         -- Reset environment
232         setfenv(self.template, oldfenv)
233 end