* libs/web: Fixed Luci template cache
[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                 end
163                                 
164                 -- Build if there is no compiled file or if compiled file is outdated
165                 if ((commt == nil) and not (tplmt == nil))
166                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
167                         local source
168                         source, err = luci.fs.readfile(sourcefile)
169                         
170                         if source then
171                                 local compiled, err = compile(source)
172                                 
173                                 luci.fs.writefile(compiledfile, luci.util.dump(compiled))
174                                 self.template = compiled
175                         end
176                 else
177                         self.template, err = loadfile(compiledfile)
178                 end
179                 
180         elseif compiler_mode == "none" then
181                 self.template, err = loadfile(self.compiledfile)
182                 
183         elseif compiler_mode == "memory" then
184                 local source
185                 source, err = luci.fs.readfile(sourcefile)
186                 if source then
187                         self.template, err = compile(source)
188                 end
189                         
190         end
191         
192         -- If we have no valid template throw error, otherwise cache the template
193         if not self.template then
194                 error(err)
195         else
196                 self.cache[name] = self.template
197         end
198 end
199
200
201 -- Renders a template
202 function Template.render(self, scope)
203         scope = scope or getfenv(2)
204         
205         -- Save old environment
206         local oldfenv = getfenv(self.template)
207         
208         -- Put our predefined objects in the scope of the template
209         luci.util.resfenv(self.template)
210         luci.util.updfenv(self.template, scope)
211         luci.util.updfenv(self.template, self.viewns)
212         
213         -- Now finally render the thing
214         self.template()
215         
216         -- Reset environment
217         setfenv(self.template, oldfenv)
218 end