d49bceac7dd8f5ef43202ff86dc2fd0f8e8282ef
[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         self.name = name
148         
149         -- Create a new namespace for this template
150         self.viewns = {}
151         
152         -- Copy over from general namespace
153         luci.util.update(self.viewns, viewns)
154         if context.viewns then
155                 luci.util.update(self.viewns, context.viewns)
156         end
157         
158         -- If we have a cached template, skip compiling and loading
159         if self.template then
160                 return
161         end
162         
163         -- Enforce cache security
164         local cdir = compiledir .. "/" .. luci.sys.process.info("uid")
165         
166         -- Compile and build
167         local sourcefile   = viewdir    .. "/" .. name .. ".htm"
168         local compiledfile = cdir .. "/" .. luci.http.urlencode(name) .. ".lua"
169         local err       
170         
171         if compiler_mode == "file" then
172                 local tplmt = luci.fs.mtime(sourcefile)
173                 local commt = luci.fs.mtime(compiledfile)
174                 
175                 if not luci.fs.mtime(cdir) then
176                         luci.fs.mkdir(cdir, true)
177                         luci.fs.chmod(luci.fs.dirname(cdir), "a+rxw")
178                 end
179                                 
180                 -- Build if there is no compiled file or if compiled file is outdated
181                 if ((commt == nil) and not (tplmt == nil))
182                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
183                         local source
184                         source, err = luci.fs.readfile(sourcefile)
185                         
186                         if source then
187                                 local compiled, err = compile(source)
188                                 
189                                 luci.fs.writefile(compiledfile, luci.util.get_bytecode(compiled))
190                                 self.template = compiled
191                         end
192                 else
193                         self.template, err = loadfile(compiledfile)
194                 end
195                 
196         elseif compiler_mode == "none" then
197                 self.template, err = loadfile(self.compiledfile)
198                 
199         elseif compiler_mode == "memory" then
200                 local source
201                 source, err = luci.fs.readfile(sourcefile)
202                 if source then
203                         self.template, err = compile(source)
204                 end
205                         
206         end
207         
208         -- If we have no valid template throw error, otherwise cache the template
209         if not self.template then
210                 error(err)
211         else
212                 self.cache[name] = self.template
213         end
214 end
215
216
217 -- Renders a template
218 function Template.render(self, scope)
219         scope = scope or getfenv(2)
220         
221         -- Save old environment
222         local oldfenv = getfenv(self.template)
223         
224         -- Put our predefined objects in the scope of the template
225         luci.util.resfenv(self.template)
226         luci.util.updfenv(self.template, scope)
227         luci.util.updfenv(self.template, self.viewns)
228         
229         -- Now finally render the thing
230         local stat, err = luci.util.copcall(self.template)
231         if not stat then
232                 error("Error in template %s: %s" % {self.name, err})
233         end
234         
235         -- Reset environment
236         setfenv(self.template, oldfenv)
237 end