Added memory tracer
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $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 --- LuCI web dispatcher.
28 local fs = require "luci.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33
34 module("luci.dispatcher", package.seeall)
35 context = luci.util.threadlocal()
36
37 authenticator = {}
38
39 -- Index table
40 local index = nil
41
42 -- Fastindex
43 local fi
44
45
46 --- Build the URL relative to the server webroot from given virtual path.
47 -- @param ...   Virtual path
48 -- @return              Relative URL
49 function build_url(...)
50         return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
51 end
52
53 --- Send a 404 error code and render the "error404" template if available.
54 -- @param message       Custom error message (optional)
55 -- @return                      false
56 function error404(message)
57         luci.http.status(404, "Not Found")
58         message = message or "Not Found"
59
60         require("luci.template")
61         if not luci.util.copcall(luci.template.render, "error404") then
62                 luci.http.prepare_content("text/plain")
63                 luci.http.write(message)
64         end
65         return false
66 end
67
68 --- Send a 500 error code and render the "error500" template if available.
69 -- @param message       Custom error message (optional)#
70 -- @return                      false
71 function error500(message)
72         luci.http.status(500, "Internal Server Error")
73
74         require("luci.template")
75         if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
76                 luci.http.prepare_content("text/plain")
77                 luci.http.write(message)
78         end
79         return false
80 end
81
82 function authenticator.htmlauth(validator, accs, default)
83         local user = luci.http.formvalue("username")
84         local pass = luci.http.formvalue("password")
85         
86         if user and validator(user, pass) then
87                 return user
88         end
89         
90         require("luci.i18n")
91         require("luci.template")
92         context.path = {}
93         luci.template.render("sysauth", {duser=default, fuser=user})
94         return false
95         
96 end
97
98 --- Dispatch an HTTP request.
99 -- @param request       LuCI HTTP Request object
100 function httpdispatch(request)
101         luci.http.context.request = request
102         context.request = {}
103         local pathinfo = request:getenv("PATH_INFO") or ""
104
105         for node in pathinfo:gmatch("[^/]+") do
106                 table.insert(context.request, node)
107         end
108
109         local stat, err = util.copcall(dispatch, context.request)
110         if not stat then
111                 error500(err)
112         end
113         
114         luci.http.close()
115
116         --context._disable_memtrace()
117 end
118
119 --- Dispatches a LuCI virtual path.
120 -- @param request       Virtual path
121 function dispatch(request)
122         --context._disable_memtrace = require "luci.debug".trap_memtrace()
123         local ctx = context
124         ctx.path = request
125         
126         require "luci.i18n".setlanguage(require "luci.config".main.lang)
127         
128         local c = ctx.tree
129         local stat
130         if not c then
131                 c = createtree()
132         end
133         
134         local track = {}
135         local args = {}
136         context.args = args
137         local n
138
139         for i, s in ipairs(request) do
140                 c = c.nodes[s]
141                 n = i
142                 if not c then
143                         break
144                 end
145
146                 util.update(track, c)
147                 
148                 if c.leaf then
149                         break
150                 end
151         end
152
153         if c and c.leaf then
154                 for j=n+1, #request do
155                         table.insert(args, request[j])
156                 end
157         end
158
159         if track.i18n then
160                 require("luci.i18n").loadc(track.i18n)
161         end
162         
163         -- Init template engine
164         if not track.notemplate then
165                 local tpl = require("luci.template")
166                 local viewns = setmetatable({}, {__index=_G})
167                 tpl.context.viewns = viewns
168                 viewns.write       = luci.http.write
169                 viewns.include     = function(name) tpl.Template(name):render(getfenv(2)) end
170                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
171                 viewns.striptags   = util.striptags
172                 viewns.controller  = luci.http.getenv("SCRIPT_NAME")
173                 viewns.media       = luci.config.main.mediaurlbase
174                 viewns.resource    = luci.config.main.resourcebase
175                 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
176         end
177         
178         track.dependent = (track.dependent ~= false)
179         assert(not track.dependent or not track.auto, "Access Violation")
180         
181         if track.sysauth then
182                 local sauth = require "luci.sauth"
183                 
184                 local authen = type(track.sysauth_authenticator) == "function"
185                  and track.sysauth_authenticator
186                  or authenticator[track.sysauth_authenticator]
187                  
188                 local def  = (type(track.sysauth) == "string") and track.sysauth
189                 local accs = def and {track.sysauth} or track.sysauth
190                 local sess = ctx.authsession or luci.http.getcookie("sysauth")
191                 sess = sess and sess:match("^[A-F0-9]+$")
192                 local user = sauth.read(sess)
193                 
194                 if not util.contains(accs, user) then
195                         if authen then
196                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
197                                 if not user or not util.contains(accs, user) then
198                                         return
199                                 else
200                                         local sid = sess or luci.sys.uniqueid(16)
201                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
202                                         if not sess then
203                                                 sauth.write(sid, user)
204                                         end
205                                         ctx.authsession = sid
206                                 end
207                         else
208                                 luci.http.status(403, "Forbidden")
209                                 return
210                         end
211                 end
212         end
213
214         if track.setgroup then
215                 luci.sys.process.setgroup(track.setgroup)
216         end
217
218         if track.setuser then
219                 luci.sys.process.setuser(track.setuser)
220         end
221
222         if c and type(c.target) == "function" then
223                 context.dispatched = c
224                 
225                 util.copcall(function()
226                         local oldenv = getfenv(c.target)
227                         local module = require(c.module)
228                         local env = setmetatable({}, {__index=
229                                 
230                         function(tbl, key)
231                                 return rawget(tbl, key) or module[key] or oldenv[key] 
232                         end})
233
234                         setfenv(c.target, env)
235                 end)
236                 
237                 c.target(unpack(args))
238         else
239                 error404()
240         end
241 end
242
243 --- Generate the dispatching index using the best possible strategy.
244 function createindex()
245         local path = luci.util.libpath() .. "/controller/"
246         local suff = ".lua"
247         
248         if luci.util.copcall(require, "luci.fastindex") then
249                 createindex_fastindex(path, suff)
250         else
251                 createindex_plain(path, suff)
252         end
253 end
254
255 --- Generate the dispatching index using the fastindex C-indexer.
256 -- @param path          Controller base directory
257 -- @param suffix        Controller file suffix
258 function createindex_fastindex(path, suffix)
259         index = {}
260                 
261         if not fi then
262                 fi = luci.fastindex.new("index")
263                 fi.add(path .. "*" .. suffix)
264                 fi.add(path .. "*/*" .. suffix)
265         end
266         fi.scan()
267         
268         for k, v in pairs(fi.indexes) do
269                 index[v[2]] = v[1]
270         end
271 end
272
273 --- Generate the dispatching index using the native file-cache based strategy.
274 -- @param path          Controller base directory
275 -- @param suffix        Controller file suffix
276 function createindex_plain(path, suffix)
277         if indexcache then
278                 local cachedate = fs.mtime(indexcache)
279                 if cachedate and cachedate > fs.mtime(path) then
280
281                         assert(
282                                 sys.process.info("uid") == fs.stat(indexcache, "uid")
283                                 and fs.stat(indexcache, "mode") == "rw-------",
284                                 "Fatal: Indexcache is not sane!"
285                         )
286
287                         index = loadfile(indexcache)()
288                         return index
289                 end             
290         end
291         
292         index = {}
293
294         local controllers = util.combine(
295                 luci.fs.glob(path .. "*" .. suffix) or {},
296                 luci.fs.glob(path .. "*/*" .. suffix) or {}
297         )
298
299         for i,c in ipairs(controllers) do
300                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
301                 local mod = require(module)
302                 local idx = mod.index
303                 
304                 if type(idx) == "function" then
305                         index[module] = idx
306                 end
307         end
308         
309         if indexcache then
310                 fs.writefile(indexcache, util.get_bytecode(index))
311                 fs.chmod(indexcache, "a-rwx,u+rw")
312         end
313 end
314
315 --- Create the dispatching tree from the index.
316 -- Build the index before if it does not exist yet.
317 function createtree()
318         if not index then
319                 createindex()
320         end
321         
322         local ctx  = context
323         local tree = {nodes={}}
324         
325         ctx.treecache = setmetatable({}, {__mode="v"})
326         ctx.tree = tree
327         
328         -- Load default translation
329         require "luci.i18n".loadc("default")
330         
331         local scope = setmetatable({}, {__index = luci.dispatcher})
332
333         for k, v in pairs(index) do
334                 scope._NAME = k
335                 setfenv(v, scope)
336                 v()
337         end
338         
339         return tree
340 end
341
342 --- Clone a node of the dispatching tree to another position.
343 -- @param       path    Virtual path destination
344 -- @param       clone   Virtual path source
345 -- @param       title   Destination node title (optional)
346 -- @param       order   Destination node order value (optional)
347 -- @return                      Dispatching tree node
348 function assign(path, clone, title, order)
349         local obj  = node(unpack(path))
350         obj.nodes  = nil
351         obj.module = nil
352         
353         obj.title = title
354         obj.order = order
355         
356         local c = context.tree
357         for k, v in ipairs(clone) do
358                 if not c.nodes[v] then
359                         c.nodes[v] = {nodes={}}
360                 end
361
362                 c = c.nodes[v]
363         end
364         
365         setmetatable(obj, {__index = c})
366         
367         return obj
368 end
369
370 --- Create a new dispatching node and define common parameters.
371 -- @param       path    Virtual path
372 -- @param       target  Target function to call when dispatched. 
373 -- @param       title   Destination node title
374 -- @param       order   Destination node order value (optional)
375 -- @return                      Dispatching tree node
376 function entry(path, target, title, order)
377         local c = node(unpack(path))
378         
379         c.target = target
380         c.title  = title
381         c.order  = order
382         c.module = getfenv(2)._NAME
383
384         return c
385 end
386
387 --- Fetch or create a new dispatching node.
388 -- @param       ...             Virtual path
389 -- @return                      Dispatching tree node
390 function node(...)
391         local c = _create_node(arg)
392
393         c.module = getfenv(2)._NAME
394         c.path = arg
395         c.auto = nil
396
397         return c
398 end
399
400 function _create_node(path, cache)
401         if #path == 0 then
402                 return context.tree
403         end
404         
405         cache = cache or context.treecache
406         local name = table.concat(path, ".")
407         local c = cache[name]
408         
409         if not c then
410                 local last = table.remove(path)
411                 c = _create_node(path, cache)
412                 
413                 local new = {nodes={}, auto=true}
414                 c.nodes[last] = new
415                 cache[name] = new
416                 
417                 return new
418         else
419                 return c
420         end
421 end
422
423 -- Subdispatchers --
424
425 --- Create a redirect to another dispatching node.
426 -- @param       ...             Virtual path destination
427 function alias(...)
428         local req = arg
429         return function()
430                 dispatch(req)
431         end
432 end
433
434 --- Rewrite the first x path values of the request.
435 -- @param       n               Number of path values to replace
436 -- @param       ...             Virtual path to replace removed path values with
437 function rewrite(n, ...)
438         local req = arg
439         return function()
440                 for i=1,n do 
441                         table.remove(context.path, 1)
442                 end
443                 
444                 for i,r in ipairs(req) do
445                         table.insert(context.path, i, r)
446                 end
447                 
448                 dispatch()
449         end
450 end
451
452 --- Create a function-call dispatching target.
453 -- @param       name    Target function of local controller 
454 -- @param       ...             Additional parameters passed to the function
455 function call(name, ...)
456         local argv = {...}
457         return function() return getfenv()[name](unpack(argv)) end
458 end
459
460 --- Create a template render dispatching target.
461 -- @param       name    Template to be rendered
462 function template(name)
463         return function()
464                 require("luci.template")
465                 luci.template.render(name)
466         end
467 end
468
469 --- Create a CBI model dispatching target.
470 -- @param       model   CBI model tpo be rendered
471 function cbi(model)
472         return function(...)
473                 require("luci.cbi")
474                 require("luci.template")
475
476                 maps = luci.cbi.load(model, ...)
477
478                 for i, res in ipairs(maps) do
479                         res:parse()
480                 end
481
482                 luci.template.render("cbi/header")
483                 for i, res in ipairs(maps) do
484                         res:render()
485                 end
486                 luci.template.render("cbi/footer")
487         end
488 end
489
490 --- Create a CBI form model dispatching target.
491 -- @param       model   CBI form model tpo be rendered
492 function form(model)
493         return function(...)
494                 require("luci.cbi")
495                 require("luci.template")
496
497                 maps = luci.cbi.load(model, ...)
498
499                 for i, res in ipairs(maps) do
500                         res:parse()
501                 end
502
503                 luci.template.render("header")
504                 for i, res in ipairs(maps) do
505                         res:render()
506                 end
507                 luci.template.render("footer")
508         end
509 end