libs/web: use new parser in non-caching mode
[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
27 local fs = require "nixio.fs"
28 local sys = require "luci.sys"
29 local util = require "luci.util"
30 local table = require "table"
31 local string = require "string"
32 local config = require "luci.config"
33 local coroutine = require "coroutine"
34 local nixio = require "nixio", require "nixio.util"
35 local tparser = require "luci.template.parser"
36
37 local tostring, pairs, loadstring = tostring, pairs, loadstring
38 local setmetatable, loadfile = setmetatable, loadfile
39 local getfenv, setfenv, rawget = getfenv, setfenv, rawget
40 local assert, type, error = assert, type, error
41
42 --- LuCI template library.
43 module "luci.template"
44
45 config.template = config.template or {}
46
47 viewdir    = config.template.viewdir or util.libpath() .. "/view"
48 compiledir = config.template.compiledir or util.libpath() .. "/view"
49
50
51 -- Compile modes:
52 -- memory:      Always compile, do not save compiled files, ignore precompiled 
53 -- file:        Compile on demand, save compiled files, update precompiled
54 compiler_mode = config.template.compiler_mode or "memory"
55
56
57 -- Define the namespace for template modules
58 context = util.threadlocal()
59
60 --- Manually  compile a given template into an executable Lua function
61 -- @param template      LuCI template
62 -- @return                      Lua template function
63 function compile(template)      
64         local expr = {}
65
66         -- Search all <% %> expressions
67         local function expr_add(ws1, skip1, command, skip2, ws2)
68                 expr[#expr+1] = command
69                 return ( #skip1 > 0 and "" or ws1 ) .. 
70                        "<%" .. tostring(#expr) .. "%>" ..
71                        ( #skip2 > 0 and "" or ws2 )
72         end
73         
74         -- Save all expressiosn to table "expr"
75         template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
76         
77         local function sanitize(s)
78                 s = "%q" % s
79                 return s:sub(2, #s-1)
80         end
81         
82         -- Escape and sanitize all the template (all non-expressions)
83         template = sanitize(template)
84
85         -- Template module header/footer declaration
86         local header = 'write("'
87         local footer = '")'
88         
89         template = header .. template .. footer
90         
91         -- Replacements
92         local r_include = '")\ninclude("%s")\nwrite("'
93         local r_i18n    = '")\nwrite(translate("%1","%2"))\nwrite("'
94         local r_i18n2   = '")\nwrite(translate("%1", ""))\nwrite("'
95         local r_pexec   = '")\nwrite(tostring(%s or ""))\nwrite("'
96         local r_exec    = '")\n%s\nwrite("'
97         
98         -- Parse the expressions
99         for k,v in pairs(expr) do
100                 local p = v:sub(1, 1)
101                 v = v:gsub("%%", "%%%%")
102                 local re = nil
103                 if p == "+" then
104                         re = r_include:format(sanitize(string.sub(v, 2)))
105                 elseif p == ":" then
106                         if v:find(" ") then
107                                 re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
108                         else
109                                 re = sanitize(v):gsub(":(.+)", r_i18n2)
110                         end
111                 elseif p == "=" then
112                         re = r_pexec:format(v:sub(2))
113                 elseif p == "#" then
114                         re = ""
115                 else
116                         re = r_exec:format(v)
117                 end
118                 template = template:gsub("<%%"..tostring(k).."%%>", re)
119         end
120
121         return loadstring(template)
122 end
123
124 --- Render a certain template.
125 -- @param name          Template name
126 -- @param scope         Scope to assign to template (optional)
127 function render(name, scope)
128         return Template(name):render(scope or getfenv(2))
129 end
130
131
132 -- Template class
133 Template = util.class()
134
135 -- Shared template cache to store templates in to avoid unnecessary reloading
136 Template.cache = setmetatable({}, {__mode = "v"})
137
138
139 -- Constructor - Reads and compiles the template on-demand
140 function Template.__init__(self, name)  
141         local function _encode_filename(str)
142
143                 local function __chrenc( chr )
144                         return "%%%02x" % string.byte( chr )
145                 end
146
147                 if type(str) == "string" then
148                         str = str:gsub(
149                                 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
150                                 __chrenc
151                         )
152                 end
153
154                 return str
155         end
156
157         self.template = self.cache[name]
158         self.name = name
159         
160         -- Create a new namespace for this template
161         self.viewns = context.viewns
162         
163         -- If we have a cached template, skip compiling and loading
164         if self.template then
165                 return
166         end
167         
168         -- Enforce cache security
169         local cdir = compiledir .. "/" .. sys.process.info("uid")
170         
171         -- Compile and build
172         local sourcefile   = viewdir    .. "/" .. name
173         local compiledfile = cdir .. "/" .. _encode_filename(name) .. ".lua"
174         local err       
175         
176         if compiler_mode == "file" then
177                 local tplmt = fs.stat(sourcefile, "mtime") or fs.stat(sourcefile .. ".htm", "mtime")
178                 local commt = fs.stat(compiledfile, "mtime")
179                 
180                 if not fs.stat(cdir, "mtime") then
181                         fs.mkdirr(cdir)
182                         fs.chmod(fs.dirname(cdir), 777)
183                 end
184                 
185                 assert(tplmt or commt, "No such template: " .. name)
186                                 
187                 -- Build if there is no compiled file or if compiled file is outdated
188                 if not commt or (commt and tplmt and commt < tplmt) then
189                         local source
190                         source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
191                         
192                         if source then
193                                 local compiled, err = compile(source)
194                                 
195                                 local f = nixio.open(compiledfile, "w", 600)
196                                 f:writeall(util.get_bytecode(compiled))
197                                 f:close()
198                                 self.template = compiled
199                         end
200                 else
201                         assert(
202                                 sys.process.info("uid") == fs.stat(compiledfile, "uid")
203                                 and fs.stat(compiledfile, "modestr") == "rw-------",
204                                 "Fatal: Cachefile is not sane!"
205                         )
206                         self.template, err = loadfile(compiledfile)
207                 end
208                 
209         elseif compiler_mode == "memory" then
210                 self.template, _, err = tparser.parse(sourcefile .. ".htm")
211         end
212         
213         -- If we have no valid template throw error, otherwise cache the template
214         if not self.template then
215                 error(err)
216         else
217                 self.cache[name] = self.template
218         end
219 end
220
221
222 -- Renders a template
223 function Template.render(self, scope)
224         scope = scope or getfenv(2)
225         
226         -- Put our predefined objects in the scope of the template
227         setfenv(self.template, setmetatable({}, {__index =
228                 function(tbl, key)
229                         return rawget(tbl, key) or self.viewns[key] or scope[key]
230                 end}))
231         
232         -- Now finally render the thing
233         local stat, err = util.copcall(self.template)
234         if not stat then
235                 error("Error in template %s: %s" % {self.name, err})
236         end
237 end