Heavy memory/performance optimizations #3
[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, rawget = getfenv, setfenv, rawget
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 --- Manually  compile a given template into an executable Lua function
59 -- @param template      LuCI template
60 -- @return                      Lua template function
61 function compile(template)      
62         local expr = {}
63
64         -- Search all <% %> expressions
65         local function expr_add(ws1, skip1, command, skip2, ws2)
66                 table.insert(expr, command)
67                 return ( #skip1 > 0 and "" or ws1 ) .. 
68                        "<%" .. tostring(#expr) .. "%>" ..
69                        ( #skip2 > 0 and "" or ws2 )
70         end
71         
72         -- Save all expressiosn to table "expr"
73         template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
74         
75         local function sanitize(s)
76                 s = "%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 (optional)
125 function render(name, scope)
126         return Template(name):render(scope or getfenv(2))
127 end
128
129
130 -- Template class
131 Template = util.class()
132
133 -- Shared template cache to store templates in to avoid unnecessary reloading
134 Template.cache = setmetatable({}, {__mode = "v"})
135
136
137 -- Constructor - Reads and compiles the template on-demand
138 function Template.__init__(self, name)  
139         local function _encode_filename(str)
140
141                 local function __chrenc( chr )
142                         return "%%%02x" % string.byte( chr )
143                 end
144
145                 if type(str) == "string" then
146                         str = str:gsub(
147                                 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
148                                 __chrenc
149                         )
150                 end
151
152                 return str
153         end
154
155         self.template = self.cache[name]
156         self.name = name
157         
158         -- Create a new namespace for this template
159         self.viewns = context.viewns
160         
161         -- If we have a cached template, skip compiling and loading
162         if self.template then
163                 return
164         end
165         
166         -- Enforce cache security
167         local cdir = compiledir .. "/" .. sys.process.info("uid")
168         
169         -- Compile and build
170         local sourcefile   = viewdir    .. "/" .. name
171         local compiledfile = cdir .. "/" .. _encode_filename(name) .. ".lua"
172         local err       
173         
174         if compiler_mode == "file" then
175                 local tplmt = fs.mtime(sourcefile) or fs.mtime(sourcefile .. ".htm")
176                 local commt = fs.mtime(compiledfile)
177                 
178                 if not fs.mtime(cdir) then
179                         fs.mkdir(cdir, true)
180                         fs.chmod(fs.dirname(cdir), "a+rxw")
181                 end
182                 
183                 assert(tplmt or commt, "No such template: " .. name)
184                                 
185                 -- Build if there is no compiled file or if compiled file is outdated
186                 if not commt or (commt  and tplmt and commt < tplmt) then
187                         local source
188                         source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
189                         
190                         if source then
191                                 local compiled, err = compile(source)
192                                 
193                                 fs.writefile(compiledfile, util.get_bytecode(compiled))
194                                 fs.chmod(compiledfile, "a-rwx,u+rw")
195                                 self.template = compiled
196                         end
197                 else
198                         assert(
199                                 sys.process.info("uid") == fs.stat(compiledfile, "uid")
200                                 and fs.stat(compiledfile, "mode") == "rw-------",
201                                 "Fatal: Cachefile is not sane!"
202                         )
203                         self.template, err = loadfile(compiledfile)
204                 end
205                 
206         elseif compiler_mode == "memory" then
207                 local source
208                 source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
209                 if source then
210                         self.template, err = compile(source)
211                 end
212                         
213         end
214         
215         -- If we have no valid template throw error, otherwise cache the template
216         if not self.template then
217                 error(err)
218         else
219                 self.cache[name] = self.template
220         end
221 end
222
223
224 -- Renders a template
225 function Template.render(self, scope)
226         scope = scope or getfenv(2)
227         
228         -- Put our predefined objects in the scope of the template
229         setfenv(self.template, setmetatable({}, {__index =
230                 function(tbl, key)
231                         return rawget(tbl, key) or self.viewns[key] or scope[key]
232                 end}))
233         
234         -- Now finally render the thing
235         local stat, err = util.copcall(self.template)
236         if not stat then
237                 error("Error in template %s: %s" % {self.name, err})
238         end
239 end