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