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