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