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