79532ddf1d1da1f5d1eef2ea863b6d74a72e6266
[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         local controllers = util.combine(
304                 luci.fs.glob(path .. "*" .. suffix) or {},
305                 luci.fs.glob(path .. "*/*" .. suffix) or {}
306         )
307
308         if indexcache then
309                 local cachedate = fs.mtime(indexcache)
310                 if cachedate then
311                         local realdate = 0
312                         for _, obj in ipairs(controllers) do
313                                 local omtime = fs.mtime(path .. "/" .. obj)
314                                 realdate = (omtime and omtime > realdate) and omtime or realdate
315                         end
316
317                         if cachedate > realdate then
318                                 assert(
319                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
320                                         and fs.stat(indexcache, "mode") == "rw-------",
321                                         "Fatal: Indexcache is not sane!"
322                                 )
323
324                                 index = loadfile(indexcache)()
325                                 return index
326                         end
327                 end
328         end
329
330         index = {}
331
332         for i,c in ipairs(controllers) do
333                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
334                 local mod = require(module)
335                 local idx = mod.index
336
337                 if type(idx) == "function" then
338                         index[module] = idx
339                 end
340         end
341
342         if indexcache then
343                 fs.writefile(indexcache, util.get_bytecode(index))
344                 fs.chmod(indexcache, "a-rwx,u+rw")
345         end
346 end
347
348 --- Create the dispatching tree from the index.
349 -- Build the index before if it does not exist yet.
350 function createtree()
351         if not index then
352                 createindex()
353         end
354
355         local ctx  = context
356         local tree = {nodes={}}
357
358         ctx.treecache = setmetatable({}, {__mode="v"})
359         ctx.tree = tree
360
361         -- Load default translation
362         require "luci.i18n".loadc("default")
363
364         local scope = setmetatable({}, {__index = luci.dispatcher})
365
366         for k, v in pairs(index) do
367                 scope._NAME = k
368                 setfenv(v, scope)
369                 v()
370         end
371
372         return tree
373 end
374
375 --- Clone a node of the dispatching tree to another position.
376 -- @param       path    Virtual path destination
377 -- @param       clone   Virtual path source
378 -- @param       title   Destination node title (optional)
379 -- @param       order   Destination node order value (optional)
380 -- @return                      Dispatching tree node
381 function assign(path, clone, title, order)
382         local obj  = node(unpack(path))
383         obj.nodes  = nil
384         obj.module = nil
385
386         obj.title = title
387         obj.order = order
388
389         setmetatable(obj, {__index = _create_node(clone)})
390
391         return obj
392 end
393
394 --- Create a new dispatching node and define common parameters.
395 -- @param       path    Virtual path
396 -- @param       target  Target function to call when dispatched.
397 -- @param       title   Destination node title
398 -- @param       order   Destination node order value (optional)
399 -- @return                      Dispatching tree node
400 function entry(path, target, title, order)
401         local c = node(unpack(path))
402
403         c.target = target
404         c.title  = title
405         c.order  = order
406         c.module = getfenv(2)._NAME
407
408         return c
409 end
410
411 --- Fetch or create a new dispatching node.
412 -- @param       ...             Virtual path
413 -- @return                      Dispatching tree node
414 function node(...)
415         local c = _create_node({...})
416
417         c.module = getfenv(2)._NAME
418         c.path = arg
419         c.auto = nil
420
421         return c
422 end
423
424 function _create_node(path, cache)
425         if #path == 0 then
426                 return context.tree
427         end
428
429         cache = cache or context.treecache
430         local name = table.concat(path, ".")
431         local c = cache[name]
432
433         if not c then
434                 local last = table.remove(path)
435                 c = _create_node(path, cache)
436
437                 local new = {nodes={}, auto=true}
438                 c.nodes[last] = new
439                 cache[name] = new
440
441                 return new
442         else
443                 return c
444         end
445 end
446
447 -- Subdispatchers --
448
449 --- Create a redirect to another dispatching node.
450 -- @param       ...             Virtual path destination
451 function alias(...)
452         local req = {...}
453         return function(...)
454                 for _, r in ipairs({...}) do
455                         req[#req+1] = r
456                 end
457
458                 dispatch(req)
459         end
460 end
461
462 --- Rewrite the first x path values of the request.
463 -- @param       n               Number of path values to replace
464 -- @param       ...             Virtual path to replace removed path values with
465 function rewrite(n, ...)
466         local req = {...}
467         return function(...)
468                 local dispatched = util.clone(context.dispatched)
469
470                 for i=1,n do
471                         table.remove(dispatched, 1)
472                 end
473
474                 for i, r in ipairs(req) do
475                         table.insert(dispatched, i, r)
476                 end
477
478                 for _, r in ipairs({...}) do
479                         dispatched[#dispatched+1] = r
480                 end
481
482                 dispatch(dispatched)
483         end
484 end
485
486 --- Create a function-call dispatching target.
487 -- @param       name    Target function of local controller
488 -- @param       ...             Additional parameters passed to the function
489 function call(name, ...)
490         local argv = {...}
491         return function(...)
492                 if #argv > 0 then 
493                         return getfenv()[name](unpack(argv), ...)
494                 else
495                         return getfenv()[name](...)
496                 end
497         end
498 end
499
500 --- Create a template render dispatching target.
501 -- @param       name    Template to be rendered
502 function template(name)
503         return function()
504                 require("luci.template")
505                 luci.template.render(name)
506         end
507 end
508
509 --- Create a CBI model dispatching target.
510 -- @param       model   CBI model to be rendered
511 function cbi(model, config)
512         config = config or {}
513         return function(...)
514                 require("luci.cbi")
515                 require("luci.template")
516                 local http = require "luci.http"
517
518                 maps = luci.cbi.load(model, ...)
519
520                 local state = nil
521
522                 for i, res in ipairs(maps) do
523                         if config.autoapply then
524                                 res.autoapply = config.autoapply
525                         end
526                         local cstate = res:parse()
527                         if not state or cstate < state then
528                                 state = cstate
529                         end
530                 end
531
532                 if config.on_success_to and state and state > 0 then
533                         luci.http.redirect(config.on_success_to)
534                         return
535                 end
536
537                 if config.state_handler then
538                         if not config.state_handler(state, maps) then
539                                 return
540                         end
541                 end
542
543                 local pageaction = true
544                 http.header("X-CBI-State", state or 0)
545                 luci.template.render("cbi/header", {state = state})
546                 for i, res in ipairs(maps) do
547                         res:render()
548                         if res.pageaction == false then
549                                 pageaction = false
550                         end
551                 end
552                 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
553         end
554 end
555
556 --- Create a CBI form model dispatching target.
557 -- @param       model   CBI form model tpo be rendered
558 function form(model)
559         return function(...)
560                 require("luci.cbi")
561                 require("luci.template")
562                 local http = require "luci.http"
563
564                 maps = luci.cbi.load(model, ...)
565
566                 local state = nil
567
568                 for i, res in ipairs(maps) do
569                         local cstate = res:parse()
570                         if not state or cstate < state then
571                                 state = cstate
572                         end
573                 end
574
575                 http.header("X-CBI-State", state or 0)
576                 luci.template.render("header")
577                 for i, res in ipairs(maps) do
578                         res:render()
579                 end
580                 luci.template.render("footer")
581         end
582 end