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