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