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