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