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