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