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