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