libs/cbi: Added magic ;-)
[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 -- Index table
37 local index = nil
38
39 -- Fastindex
40 local fi
41
42
43 --- Build the URL relative to the server webroot from given virtual path.
44 -- @param ...   Virtual path
45 -- @return              Relative URL
46 function build_url(...)
47         return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
48 end
49
50 --- Send a 404 error code and render the "error404" template if available.
51 -- @param message       Custom error message (optional)
52 -- @return                      false
53 function error404(message)
54         luci.http.status(404, "Not Found")
55         message = message or "Not Found"
56
57         require("luci.template")
58         if not luci.util.copcall(luci.template.render, "error404") then
59                 luci.http.prepare_content("text/plain")
60                 luci.http.write(message)
61         end
62         return false
63 end
64
65 --- Send a 500 error code and render the "error500" template if available.
66 -- @param message       Custom error message (optional)#
67 -- @return                      false
68 function error500(message)
69         luci.http.status(500, "Internal Server Error")
70
71         require("luci.template")
72         if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
73                 luci.http.prepare_content("text/plain")
74                 luci.http.write(message)
75         end
76         return false
77 end
78
79 --- Render and evaluate the system authentication login form.
80 -- @param default       Default username
81 -- @return                      Authentication status
82 function sysauth(default)
83         local user = luci.http.formvalue("username")
84         local pass = luci.http.formvalue("password")
85         
86         if user and luci.sys.user.checkpasswd(user, pass) then
87                 local sid = luci.sys.uniqueid(16)
88                 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
89                 luci.sauth.write(sid, user)
90                 return true
91         else
92                 require("luci.i18n")
93                 require("luci.template")
94                 context.path = {}
95                 luci.template.render("sysauth", {duser=default, fuser=user})
96                 return false
97         end
98 end
99
100 --- Dispatch an HTTP request.
101 -- @param request       LuCI HTTP Request object
102 function httpdispatch(request)
103         luci.http.context.request = request
104         context.request = {}
105         local pathinfo = request:getenv("PATH_INFO") or ""
106
107         for node in pathinfo:gmatch("[^/]+") do
108                 table.insert(context.request, node)
109         end
110
111         dispatch(context.request)
112         luci.http.close()
113 end
114
115 --- Dispatches a LuCI virtual path.
116 -- @param request       Virtual path
117 function dispatch(request)
118         context.path = request
119         
120         require("luci.i18n")
121         luci.i18n.setlanguage(require("luci.config").main.lang)
122         
123         if not context.tree then
124                 createtree()
125         end
126         
127         local c = context.tree
128         local track = {}
129         local args = {}
130         local n
131
132         for i, s in ipairs(request) do
133                 c = c.nodes[s]
134                 n = i
135                 if not c or c.leaf then
136                         break
137                 end
138
139                 for k, v in pairs(c) do
140                         track[k] = v
141                 end
142         end
143
144         if c and c.leaf then
145                 for j=n+1, #request do
146                         table.insert(args, request[j])
147                 end
148         end
149
150         if track.i18n then
151                 require("luci.i18n").loadc(track.i18n)
152         end
153         
154         -- Init template engine
155         local tpl = require("luci.template")
156         local viewns = {}
157         tpl.context.viewns = viewns
158         viewns.write       = luci.http.write
159         viewns.translate   = function(...) return require("luci.i18n").translate(...) end
160         viewns.controller  = luci.http.getenv("SCRIPT_NAME")
161         viewns.media       = luci.config.main.mediaurlbase
162         viewns.resource    = luci.config.main.resourcebase
163         viewns.REQUEST_URI = luci.http.getenv("SCRIPT_NAME") .. (luci.http.getenv("PATH_INFO") or "")
164         
165         if track.dependent then
166                 local stat, err = pcall(assert, not track.auto)
167                 if not stat then
168                         error500(err)
169                         return
170                 end
171         end
172         
173         if track.sysauth then
174                 require("luci.sauth")
175                 local def  = (type(track.sysauth) == "string") and track.sysauth
176                 local accs = def and {track.sysauth} or track.sysauth
177                 local user = luci.sauth.read(luci.http.getcookie("sysauth"))
178                 
179                 
180                 if not luci.util.contains(accs, user) then
181                         if not sysauth(def) then
182                                 return
183                         end
184                 end
185         end
186
187         if track.setgroup then
188                 luci.sys.process.setgroup(track.setgroup)
189         end
190
191         if track.setuser then
192                 luci.sys.process.setuser(track.setuser)
193         end
194
195         if c and type(c.target) == "function" then
196                 context.dispatched = c
197                 stat, mod = luci.util.copcall(require, c.module)
198                 if stat then
199                         luci.util.updfenv(c.target, mod)
200                 end
201                 
202                 stat, err = luci.util.copcall(c.target, unpack(args))
203                 if not stat then
204                         error500(err)
205                 end
206         else
207                 error404()
208         end
209 end
210
211 --- Generate the dispatching index using the best possible strategy.
212 function createindex()
213         local path = luci.util.libpath() .. "/controller/"
214         local suff = ".lua"
215         
216         if luci.util.copcall(require, "luci.fastindex") then
217                 createindex_fastindex(path, suff)
218         else
219                 createindex_plain(path, suff)
220         end
221 end
222
223 --- Generate the dispatching index using the fastindex C-indexer.
224 -- @param path          Controller base directory
225 -- @param suffix        Controller file suffix
226 function createindex_fastindex(path, suffix)
227         index = {}
228                 
229         if not fi then
230                 fi = luci.fastindex.new("index")
231                 fi.add(path .. "*" .. suffix)
232                 fi.add(path .. "*/*" .. suffix)
233         end
234         fi.scan()
235         
236         for k, v in pairs(fi.indexes) do
237                 index[v[2]] = v[1]
238         end
239 end
240
241 --- Generate the dispatching index using the native file-cache based strategy.
242 -- @param path          Controller base directory
243 -- @param suffix        Controller file suffix
244 function createindex_plain(path, suffix)
245         index = {}
246
247         local cache = nil 
248         
249         local controllers = luci.util.combine(
250                 luci.fs.glob(path .. "*" .. suffix) or {},
251                 luci.fs.glob(path .. "*/*" .. suffix) or {}
252         )
253         
254         if indexcache then
255                 cache = luci.fs.mtime(indexcache)
256                 
257                 if not cache then
258                         luci.fs.mkdir(indexcache)
259                         luci.fs.chmod(indexcache, "a=,u=rwx")
260                         cache = luci.fs.mtime(indexcache)
261                 end
262         end
263
264         for i,c in ipairs(controllers) do
265                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
266                 local cachefile
267                 local stime
268                 local ctime
269                 
270                 if cache then
271                         cachefile = indexcache .. "/" .. module
272                         stime = luci.fs.mtime(c) or 0
273                         ctime = luci.fs.mtime(cachefile) or 0
274                 end
275                 
276                 if not cache or stime > ctime then 
277                         stat, mod = luci.util.copcall(require, module)
278         
279                         if stat and mod and type(mod.index) == "function" then
280                                 index[module] = mod.index
281                                 
282                                 if cache then
283                                         luci.fs.writefile(cachefile, luci.util.get_bytecode(mod.index))
284                                 end
285                         end
286                 else
287                         index[module] = loadfile(cachefile)
288                 end
289         end
290 end
291
292 --- Create the dispatching tree from the index.
293 -- Build the index before if it does not exist yet.
294 function createtree()
295         if not index then
296                 createindex()
297         end
298         
299         context.tree = {nodes={}}
300         require("luci.i18n")
301                 
302         -- Load default translation
303         luci.i18n.loadc("default")
304         
305         local scope = luci.util.clone(_G)
306         for k,v in pairs(luci.dispatcher) do
307                 if type(v) == "function" then
308                         scope[k] = v
309                 end
310         end
311
312         for k, v in pairs(index) do
313                 scope._NAME = k
314                 setfenv(v, scope)
315
316                 local stat, err = luci.util.copcall(v)
317                 if not stat then
318                         error500("createtree failed: " .. k .. ": " .. err)
319                         luci.http.close()
320                         os.exit(1)
321                 end
322         end
323 end
324
325 --- Clone a node of the dispatching tree to another position.
326 -- @param       path    Virtual path destination
327 -- @param       clone   Virtual path source
328 -- @param       title   Destination node title (optional)
329 -- @param       order   Destination node order value (optional)
330 -- @return                      Dispatching tree node
331 function assign(path, clone, title, order)
332         local obj  = node(unpack(path))
333         obj.nodes  = nil
334         obj.module = nil
335         
336         obj.title = title
337         obj.order = order
338         
339         local c = context.tree
340         for k, v in ipairs(clone) do
341                 if not c.nodes[v] then
342                         c.nodes[v] = {nodes={}}
343                 end
344
345                 c = c.nodes[v]
346         end
347         
348         setmetatable(obj, {__index = c})
349         
350         return obj
351 end
352
353 --- Create a new dispatching node and define common parameters.
354 -- @param       path    Virtual path
355 -- @param       target  Target function to call when dispatched. 
356 -- @param       title   Destination node title
357 -- @param       order   Destination node order value (optional)
358 -- @return                      Dispatching tree node
359 function entry(path, target, title, order)
360         local c = node(unpack(path))
361         
362         c.target = target
363         c.title  = title
364         c.order  = order
365         c.module = getfenv(2)._NAME
366
367         return c
368 end
369
370 --- Fetch or create a new dispatching node.
371 -- @param       ...             Virtual path
372 -- @return                      Dispatching tree node
373 function node(...)
374         local c = context.tree
375         arg.n = nil
376
377         for k,v in ipairs(arg) do
378                 if not c.nodes[v] then
379                         c.nodes[v] = {nodes={}, auto=true}
380                 end
381
382                 c = c.nodes[v]
383         end
384
385         c.module = getfenv(2)._NAME
386         c.path = arg
387         c.auto = nil
388
389         return c
390 end
391
392 -- Subdispatchers --
393
394 --- Create a redirect to another dispatching node.
395 -- @param       ...             Virtual path destination
396 function alias(...)
397         local req = arg
398         return function()
399                 dispatch(req)
400         end
401 end
402
403 --- Rewrite the first x path values of the request.
404 -- @param       n               Number of path values to replace
405 -- @param       ...             Virtual path to replace removed path values with
406 function rewrite(n, ...)
407         local req = arg
408         return function()
409                 for i=1,n do 
410                         table.remove(context.path, 1)
411                 end
412                 
413                 for i,r in ipairs(req) do
414                         table.insert(context.path, i, r)
415                 end
416                 
417                 dispatch()
418         end
419 end
420
421 --- Create a function-call dispatching target.
422 -- @param       name    Target function of local controller 
423 -- @param       ...             Additional parameters passed to the function
424 function call(name, ...)
425         local argv = {...}
426         return function() return getfenv()[name](unpack(argv)) end
427 end
428
429 --- Create a template render dispatching target.
430 -- @param       name    Template to be rendered
431 function template(name)
432         require("luci.template")
433         return function() luci.template.render(name) end
434 end
435
436 --- Create a CBI model dispatching target.
437 -- @param       model   CBI model tpo be rendered
438 function cbi(model)
439         require("luci.cbi")
440         require("luci.template")
441
442         return function(...)
443                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
444                 if not stat then
445                         error500(maps)
446                         return true
447                 end
448
449                 for i, res in ipairs(maps) do
450                         local stat, err = luci.util.copcall(res.parse, res)
451                         if not stat then
452                                 error500(err)
453                                 return true
454                         end
455                 end
456
457                 luci.template.render("cbi/header")
458                 for i, res in ipairs(maps) do
459                         res:render()
460                 end
461                 luci.template.render("cbi/footer")
462         end
463 end