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