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