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