* Fixed last commit
[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 -- Enforce cache security
39 compiledir = compiledir .. "/" .. luci.sys.process.info("uid")
40
41
42 -- Compile modes:
43 -- none:        Never compile, only use precompiled data from files
44 -- memory:      Always compile, do not save compiled files, ignore precompiled 
45 -- file:        Compile on demand, save compiled files, update precompiled
46 compiler_mode = luci.config.template.compiler_mode or "memory"
47
48
49 -- Define the namespace for template modules
50 viewns = {
51         write      = io.write,
52         include    = function(name) Template(name):render(getfenv(2)) end,      
53 }
54
55 -- Compiles a given template into an executable Lua module
56 function compile(template)      
57         -- Search all <% %> expressions (remember: Lua table indexes begin with #1)
58         local function expr_add(command)
59                 table.insert(expr, command)
60                 return "<%" .. tostring(#expr) .. "%>"
61         end
62         
63         -- As "expr" should be local, we have to assign it to the "expr_add" scope 
64         local expr = {}
65         luci.util.extfenv(expr_add, "expr", expr)
66         
67         -- Save all expressiosn to table "expr"
68         template = template:gsub("<%%(.-)%%>", expr_add)
69         
70         local function sanitize(s)
71                 s = luci.util.escape(s)
72                 s = luci.util.escape(s, "'")
73                 s = luci.util.escape(s, "\n")
74                 return s
75         end
76         
77         -- Escape and sanitize all the template (all non-expressions)
78         template = sanitize(template)
79
80         -- Template module header/footer declaration
81         local header = "write('"
82         local footer = "')"
83         
84         template = header .. template .. footer
85         
86         -- Replacements
87         local r_include = "')\ninclude('%s')\nwrite('"
88         local r_i18n    = "'..translate('%1','%2')..'"
89         local r_pexec   = "'..(%s or '')..'"
90         local r_exec    = "')\n%s\nwrite('"
91         
92         -- Parse the expressions
93         for k,v in pairs(expr) do
94                 local p = v:sub(1, 1)
95                 local re = nil
96                 if p == "+" then
97                         re = r_include:format(sanitize(string.sub(v, 2)))
98                 elseif p == ":" then
99                         re = sanitize(v):gsub(":(.-) (.+)", r_i18n)
100                 elseif p == "=" then
101                         re = r_pexec:format(v:sub(2))
102                 else
103                         re = r_exec:format(v)
104                 end
105                 template = template:gsub("<%%"..tostring(k).."%%>", re)
106         end
107
108         return loadstring(template)
109 end
110
111 -- Oldstyle render shortcut
112 function render(name, scope, ...)
113         scope = scope or getfenv(2)
114         local s, t = pcall(Template, name)
115         if not s then
116                 error(t)
117         else
118                 t:render(scope, ...)
119         end
120 end
121
122
123 -- Template class
124 Template = luci.util.class()
125
126 -- Shared template cache to store templates in to avoid unnecessary reloading
127 Template.cache = {}
128
129
130 -- Constructor - Reads and compiles the template on-demand
131 function Template.__init__(self, name)  
132         if self.cache[name] then
133                 self.template = self.cache[name]
134         else
135                 self.template = nil
136         end
137         
138         -- Create a new namespace for this template
139         self.viewns = {}
140         
141         -- Copy over from general namespace
142         for k, v in pairs(viewns) do
143                 self.viewns[k] = v
144         end     
145         
146         -- If we have a cached template, skip compiling and loading
147         if self.template then
148                 return
149         end
150         
151         -- Compile and build
152         local sourcefile   = viewdir    .. "/" .. name .. ".htm"
153         local compiledfile = compiledir .. "/" .. 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(compiledir) then
161                         luci.fs.mkdir(compiledir, true)
162                         luci.fs.chmod(luci.fs.dirname(compiledir), "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