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