libs: Fixed serialization stuff
[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(ws1, skip1, command, skip2, ws2)
57                 table.insert(expr, command)
58                 return ( #skip1 > 0 and "" or ws1 ) .. 
59                        "<%" .. tostring(#expr) .. "%>" ..
60                        ( #skip2 > 0 and "" or ws2 )
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("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
69         
70         local function sanitize(s)
71                 s = string.format("%q", s)
72                 return s:sub(2, #s-1)
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                 v = v:gsub("%%", "%%%%")
95                 local re = nil
96                 if p == "+" then
97                         re = r_include:format(sanitize(string.sub(v, 2)))
98                 elseif p == ":" then
99                         if v:find(" ") then
100                                 re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
101                         else
102                                 re = sanitize(v):gsub(":(.+)", r_i18n2)
103                         end
104                 elseif p == "=" then
105                         re = r_pexec:format(v:sub(2))
106                 elseif p == "#" then
107                         re = ""
108                 else
109                         re = r_exec:format(v)
110                 end
111                 template = template:gsub("<%%"..tostring(k).."%%>", re)
112         end
113
114         return loadstring(template)
115 end
116
117 -- Oldstyle render shortcut
118 function render(name, scope, ...)
119         scope = scope or getfenv(2)
120         local s, t = luci.util.copcall(Template, name)
121         if not s then
122                 error(t)
123         else
124                 t:render(scope, ...)
125         end
126 end
127
128
129 -- Template class
130 Template = luci.util.class()
131
132 -- Shared template cache to store templates in to avoid unnecessary reloading
133 Template.cache = {}
134 setmetatable(Template.cache, {__mode = "v"})
135
136
137 -- Constructor - Reads and compiles the template on-demand
138 function Template.__init__(self, name)  
139         self.template = self.cache[name]
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.get_bytecode(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