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