Added custom filename support to luci.template
[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, srcfile, comfile)        
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   = srcfile or (viewdir    .. "/" .. name .. ".htm")
181         local compiledfile = comfile or (cdir .. "/" .. _encode_filename(name) .. ".lua")
182         local err       
183         
184         if compiler_mode == "file" then
185                 local tplmt = fs.mtime(sourcefile)
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                 -- Build if there is no compiled file or if compiled file is outdated
194                 if ((commt == nil) and not (tplmt == nil))
195                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
196                         local source
197                         source, err = fs.readfile(sourcefile)
198                         
199                         if source then
200                                 local compiled, err = compile(source)
201                                 
202                                 fs.writefile(compiledfile, util.get_bytecode(compiled))
203                                 fs.chmod(compiledfile, "a-rwx,u+rw")
204                                 self.template = compiled
205                         end
206                 else
207                         assert(
208                                 sys.process.info("uid") == fs.stat(compiledfile, "uid")
209                                 and fs.stat(compiledfile, "mode") == "rw-------",
210                                 "Fatal: Cachefile is not sane!"
211                         )
212                         self.template, err = loadfile(compiledfile)
213                 end
214                 
215         elseif compiler_mode == "memory" then
216                 local source
217                 source, err = fs.readfile(sourcefile)
218                 if source then
219                         self.template, err = compile(source)
220                 end
221                         
222         end
223         
224         -- If we have no valid template throw error, otherwise cache the template
225         if not self.template then
226                 error(err)
227         else
228                 self.cache[name] = self.template
229         end
230 end
231
232
233 -- Renders a template
234 function Template.render(self, scope)
235         scope = scope or getfenv(2)
236         
237         -- Save old environment
238         local oldfenv = getfenv(self.template)
239         
240         -- Put our predefined objects in the scope of the template
241         util.resfenv(self.template)
242         util.updfenv(self.template, scope)
243         util.updfenv(self.template, self.viewns)
244         
245         -- Now finally render the thing
246         local stat, err = util.copcall(self.template)
247         if not stat then
248                 setfenv(self.template, oldfenv)
249                 error("Error in template %s: %s" % {self.name, chunk})
250         end
251         
252         -- Reset environment
253         setfenv(self.template, oldfenv)
254 end