Tuned dispatcher behaviour
[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         setmetatable(obj, {__index = _create_node(clone)})
357         
358         return obj
359 end
360
361 --- Create a new dispatching node and define common parameters.
362 -- @param       path    Virtual path
363 -- @param       target  Target function to call when dispatched. 
364 -- @param       title   Destination node title
365 -- @param       order   Destination node order value (optional)
366 -- @return                      Dispatching tree node
367 function entry(path, target, title, order)
368         local c = node(unpack(path))
369         
370         c.target = target
371         c.title  = title
372         c.order  = order
373         c.module = getfenv(2)._NAME
374
375         return c
376 end
377
378 --- Fetch or create a new dispatching node.
379 -- @param       ...             Virtual path
380 -- @return                      Dispatching tree node
381 function node(...)
382         local c = _create_node({...})
383
384         c.module = getfenv(2)._NAME
385         c.path = arg
386         c.auto = nil
387
388         return c
389 end
390
391 function _create_node(path, cache)
392         if #path == 0 then
393                 return context.tree
394         end
395         
396         cache = cache or context.treecache
397         local name = table.concat(path, ".")
398         local c = cache[name]
399         
400         if not c then
401                 local last = table.remove(path)
402                 c = _create_node(path, cache)
403                 
404                 local new = {nodes={}, auto=true}
405                 c.nodes[last] = new
406                 cache[name] = new
407                 
408                 return new
409         else
410                 return c
411         end
412 end
413
414 -- Subdispatchers --
415
416 --- Create a redirect to another dispatching node.
417 -- @param       ...             Virtual path destination
418 function alias(...)
419         local req = arg
420         return function()
421                 dispatch(req)
422         end
423 end
424
425 --- Rewrite the first x path values of the request.
426 -- @param       n               Number of path values to replace
427 -- @param       ...             Virtual path to replace removed path values with
428 function rewrite(n, ...)
429         local req = arg
430         return function()
431                 for i=1,n do 
432                         table.remove(context.path, 1)
433                 end
434                 
435                 for i,r in ipairs(req) do
436                         table.insert(context.path, i, r)
437                 end
438                 
439                 dispatch()
440         end
441 end
442
443 --- Create a function-call dispatching target.
444 -- @param       name    Target function of local controller 
445 -- @param       ...             Additional parameters passed to the function
446 function call(name, ...)
447         local argv = {...}
448         return function() return getfenv()[name](unpack(argv)) end
449 end
450
451 --- Create a template render dispatching target.
452 -- @param       name    Template to be rendered
453 function template(name)
454         return function()
455                 require("luci.template")
456                 luci.template.render(name)
457         end
458 end
459
460 --- Create a CBI model dispatching target.
461 -- @param       model   CBI model tpo be rendered
462 function cbi(model)
463         return function(...)
464                 require("luci.cbi")
465                 require("luci.template")
466
467                 maps = luci.cbi.load(model, ...)
468
469                 for i, res in ipairs(maps) do
470                         res:parse()
471                 end
472
473                 luci.template.render("cbi/header")
474                 for i, res in ipairs(maps) do
475                         res:render()
476                 end
477                 luci.template.render("cbi/footer")
478         end
479 end
480
481 --- Create a CBI form model dispatching target.
482 -- @param       model   CBI form model tpo be rendered
483 function form(model)
484         return function(...)
485                 require("luci.cbi")
486                 require("luci.template")
487
488                 maps = luci.cbi.load(model, ...)
489
490                 for i, res in ipairs(maps) do
491                         res:parse()
492                 end
493
494                 luci.template.render("header")
495                 for i, res in ipairs(maps) do
496                         res:render()
497                 end
498                 luci.template.render("footer")
499         end
500 end