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