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