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