Backport Skip-Button support
[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 = 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         local path = {...}
51         local sn = http.getenv("SCRIPT_NAME") or ""
52         for k, v in pairs(context.urltoken) do
53                 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
54         end
55         return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
56 end
57
58 --- Send a 404 error code and render the "error404" template if available.
59 -- @param message       Custom error message (optional)
60 -- @return                      false
61 function error404(message)
62         luci.http.status(404, "Not Found")
63         message = message or "Not Found"
64
65         require("luci.template")
66         if not luci.util.copcall(luci.template.render, "error404") then
67                 luci.http.prepare_content("text/plain")
68                 luci.http.write(message)
69         end
70         return false
71 end
72
73 --- Send a 500 error code and render the "error500" template if available.
74 -- @param message       Custom error message (optional)#
75 -- @return                      false
76 function error500(message)
77         luci.util.perror(message)
78         if not context.template_header_sent then
79                 luci.http.status(500, "Internal Server Error")
80                 luci.http.prepare_content("text/plain")
81                 luci.http.write(message)
82         else
83                 require("luci.template")
84                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
85                         luci.http.prepare_content("text/plain")
86                         luci.http.write(message)
87                 end
88         end
89         return false
90 end
91
92 function authenticator.htmlauth(validator, accs, default)
93         local user = luci.http.formvalue("username")
94         local pass = luci.http.formvalue("password")
95
96         if user and validator(user, pass) then
97                 return user
98         end
99
100         require("luci.i18n")
101         require("luci.template")
102         context.path = {}
103         luci.template.render("sysauth", {duser=default, fuser=user})
104         return false
105
106 end
107
108 --- Dispatch an HTTP request.
109 -- @param request       LuCI HTTP Request object
110 function httpdispatch(request)
111         luci.http.context.request = request
112         context.request = {}
113         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
114
115         for node in pathinfo:gmatch("[^/]+") do
116                 table.insert(context.request, node)
117         end
118
119         local stat, err = util.coxpcall(function()
120                 dispatch(context.request)
121         end, error500)
122
123         luci.http.close()
124
125         --context._disable_memtrace()
126 end
127
128 --- Dispatches a LuCI virtual path.
129 -- @param request       Virtual path
130 function dispatch(request)
131         --context._disable_memtrace = require "luci.debug".trap_memtrace()
132         local ctx = context
133         ctx.path = request
134         ctx.urltoken   = ctx.urltoken or {}
135
136         local conf = require "luci.config"
137         assert(conf.main,
138                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
139
140         local lang = conf.main.lang
141         if lang == "auto" then
142                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
143                 for lpat in aclang:gmatch("[%w-]+") do
144                         lpat = lpat and lpat:gsub("-", "_")
145                         if conf.languages[lpat] then
146                                 lang = lpat
147                                 break
148                         end
149                 end
150         end
151         require "luci.i18n".setlanguage(lang)
152
153         local c = ctx.tree
154         local stat
155         if not c then
156                 c = createtree()
157         end
158
159         local track = {}
160         local args = {}
161         ctx.args = args
162         ctx.requestargs = ctx.requestargs or args
163         local n
164         local t = true
165         local token = ctx.urltoken
166         local preq = {}
167         local freq = {}
168
169         for i, s in ipairs(request) do
170                 local tkey, tval
171                 if t then
172                         tkey, tval = s:match(";(%w+)=(.*)")
173                 end
174
175                 if tkey then
176                         token[tkey] = tval
177                 else
178                         t = false
179                         preq[#preq+1] = s
180                         freq[#freq+1] = s
181                         c = c.nodes[s]
182                         n = i
183                         if not c then
184                                 break
185                         end
186
187                         util.update(track, c)
188
189                         if c.leaf then
190                                 break
191                         end
192                 end
193         end
194
195         if c and c.leaf then
196                 for j=n+1, #request do
197                         args[#args+1] = request[j]
198                         freq[#freq+1] = request[j]
199                 end
200         end
201
202         ctx.requestpath = freq
203         ctx.path = preq
204
205         if track.i18n then
206                 require("luci.i18n").loadc(track.i18n)
207         end
208
209         -- Init template engine
210         if (c and c.index) or not track.notemplate then
211                 local tpl = require("luci.template")
212                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
213                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
214                         media = nil
215                         for name, theme in pairs(luci.config.themes) do
216                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
217                                  "themes/%s/header" % fs.basename(theme)) then
218                                         media = theme
219                                 end
220                         end
221                         assert(media, "No valid theme found")
222                 end
223
224                 local viewns = setmetatable({}, {__index=function(table, key)
225                         if key == "controller" then
226                                 return build_url()
227                         elseif key == "REQUEST_URI" then
228                                 return build_url(unpack(ctx.requestpath))
229                         else
230                                 return rawget(table, key) or _G[key]
231                         end
232                 end})
233                 tpl.context.viewns = viewns
234                 viewns.write       = luci.http.write
235                 viewns.include     = function(name) tpl.Template(name):render(getfenv(2)) end
236                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
237                 viewns.striptags   = util.striptags
238                 viewns.media       = media
239                 viewns.theme       = fs.basename(media)
240                 viewns.resource    = luci.config.main.resourcebase
241         end
242
243         track.dependent = (track.dependent ~= false)
244         assert(not track.dependent or not track.auto, "Access Violation")
245
246         if track.sysauth then
247                 local sauth = require "luci.sauth"
248
249                 local authen = type(track.sysauth_authenticator) == "function"
250                  and track.sysauth_authenticator
251                  or authenticator[track.sysauth_authenticator]
252
253                 local def  = (type(track.sysauth) == "string") and track.sysauth
254                 local accs = def and {track.sysauth} or track.sysauth
255                 local sess = ctx.authsession
256                 local verifytoken = false
257                 if not sess then
258                         sess = luci.http.getcookie("sysauth")
259                         sess = sess and sess:match("^[A-F0-9]+$")
260                         verifytoken = true
261                 end
262
263                 local sdat = sauth.read(sess)
264                 local user
265
266                 if sdat then
267                         sdat = loadstring(sdat)()
268                         if not verifytoken or ctx.urltoken.stok == sdat.token then
269                                 user = sdat.user
270                         end
271                 end
272
273                 if not util.contains(accs, user) then
274                         if authen then
275                                 ctx.urltoken.stok = nil
276                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
277                                 if not user or not util.contains(accs, user) then
278                                         return
279                                 else
280                                         local sid = sess or luci.sys.uniqueid(16)
281                                         if not sess then
282                                                 local token = luci.sys.uniqueid(16)
283                                                 sauth.write(sid, util.get_bytecode({
284                                                         user=user,
285                                                         token=token,
286                                                         secret=luci.sys.uniqueid(16)
287                                                 }))
288                                                 ctx.urltoken.stok = token
289                                         end
290                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
291                                         ctx.authsession = sid
292                                 end
293                         else
294                                 luci.http.status(403, "Forbidden")
295                                 return
296                         end
297                 else
298                         ctx.authsession = sess
299                 end
300         end
301
302         if track.setgroup then
303                 luci.sys.process.setgroup(track.setgroup)
304         end
305
306         if track.setuser then
307                 luci.sys.process.setuser(track.setuser)
308         end
309
310         local target = nil
311         if c then
312                 if type(c.target) == "function" then
313                         target = c.target
314                 elseif type(c.target) == "table" then
315                         target = c.target.target
316                 end
317         end
318
319         if c and (c.index or type(target) == "function") then
320                 ctx.dispatched = c
321                 ctx.requested = ctx.requested or ctx.dispatched
322         end
323
324         if c and c.index then
325                 local tpl = require "luci.template"
326
327                 if util.copcall(tpl.render, "indexer", {}) then
328                         return true
329                 end
330         end
331
332         if type(target) == "function" then
333                 util.copcall(function()
334                         local oldenv = getfenv(target)
335                         local module = require(c.module)
336                         local env = setmetatable({}, {__index=
337
338                         function(tbl, key)
339                                 return rawget(tbl, key) or module[key] or oldenv[key]
340                         end})
341
342                         setfenv(target, env)
343                 end)
344
345                 if type(c.target) == "table" then
346                         target(c.target, unpack(args))
347                 else
348                         target(unpack(args))
349                 end
350         else
351                 error404()
352         end
353 end
354
355 --- Generate the dispatching index using the best possible strategy.
356 function createindex()
357         local path = luci.util.libpath() .. "/controller/"
358         local suff = ".lua"
359
360         if luci.util.copcall(require, "luci.fastindex") then
361                 createindex_fastindex(path, suff)
362         else
363                 createindex_plain(path, suff)
364         end
365 end
366
367 --- Generate the dispatching index using the fastindex C-indexer.
368 -- @param path          Controller base directory
369 -- @param suffix        Controller file suffix
370 function createindex_fastindex(path, suffix)
371         index = {}
372
373         if not fi then
374                 fi = luci.fastindex.new("index")
375                 fi.add(path .. "*" .. suffix)
376                 fi.add(path .. "*/*" .. suffix)
377         end
378         fi.scan()
379
380         for k, v in pairs(fi.indexes) do
381                 index[v[2]] = v[1]
382         end
383 end
384
385 --- Generate the dispatching index using the native file-cache based strategy.
386 -- @param path          Controller base directory
387 -- @param suffix        Controller file suffix
388 function createindex_plain(path, suffix)
389         local controllers = util.combine(
390                 luci.fs.glob(path .. "*" .. suffix) or {},
391                 luci.fs.glob(path .. "*/*" .. suffix) or {}
392         )
393
394         if indexcache then
395                 local cachedate = fs.mtime(indexcache)
396                 if cachedate then
397                         local realdate = 0
398                         for _, obj in ipairs(controllers) do
399                                 local omtime = fs.mtime(path .. "/" .. obj)
400                                 realdate = (omtime and omtime > realdate) and omtime or realdate
401                         end
402
403                         if cachedate > realdate then
404                                 assert(
405                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
406                                         and fs.stat(indexcache, "mode") == "rw-------",
407                                         "Fatal: Indexcache is not sane!"
408                                 )
409
410                                 index = loadfile(indexcache)()
411                                 return index
412                         end
413                 end
414         end
415
416         index = {}
417
418         for i,c in ipairs(controllers) do
419                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
420                 local mod = require(module)
421                 local idx = mod.index
422
423                 if type(idx) == "function" then
424                         index[module] = idx
425                 end
426         end
427
428         if indexcache then
429                 fs.writefile(indexcache, util.get_bytecode(index))
430                 fs.chmod(indexcache, "a-rwx,u+rw")
431         end
432 end
433
434 --- Create the dispatching tree from the index.
435 -- Build the index before if it does not exist yet.
436 function createtree()
437         if not index then
438                 createindex()
439         end
440
441         local ctx  = context
442         local tree = {nodes={}}
443         local modi = {}
444
445         ctx.treecache = setmetatable({}, {__mode="v"})
446         ctx.tree = tree
447         ctx.modifiers = modi
448
449         -- Load default translation
450         require "luci.i18n".loadc("default")
451
452         local scope = setmetatable({}, {__index = luci.dispatcher})
453
454         for k, v in pairs(index) do
455                 scope._NAME = k
456                 setfenv(v, scope)
457                 v()
458         end
459
460         local function modisort(a,b)
461                 return modi[a].order < modi[b].order
462         end
463
464         for _, v in util.spairs(modi, modisort) do
465                 scope._NAME = v.module
466                 setfenv(v.func, scope)
467                 v.func()
468         end
469
470         return tree
471 end
472
473 --- Register a tree modifier.
474 -- @param       func    Modifier function
475 -- @param       order   Modifier order value (optional)
476 function modifier(func, order)
477         context.modifiers[#context.modifiers+1] = {
478                 func = func,
479                 order = order or 0,
480                 module
481                         = getfenv(2)._NAME
482         }
483 end
484
485 --- Clone a node of the dispatching tree to another position.
486 -- @param       path    Virtual path destination
487 -- @param       clone   Virtual path source
488 -- @param       title   Destination node title (optional)
489 -- @param       order   Destination node order value (optional)
490 -- @return                      Dispatching tree node
491 function assign(path, clone, title, order)
492         local obj  = node(unpack(path))
493         obj.nodes  = nil
494         obj.module = nil
495
496         obj.title = title
497         obj.order = order
498
499         setmetatable(obj, {__index = _create_node(clone)})
500
501         return obj
502 end
503
504 --- Create a new dispatching node and define common parameters.
505 -- @param       path    Virtual path
506 -- @param       target  Target function to call when dispatched.
507 -- @param       title   Destination node title
508 -- @param       order   Destination node order value (optional)
509 -- @return                      Dispatching tree node
510 function entry(path, target, title, order)
511         local c = node(unpack(path))
512
513         c.target = target
514         c.title  = title
515         c.order  = order
516         c.module = getfenv(2)._NAME
517
518         return c
519 end
520
521 --- Fetch or create a dispatching node without setting the target module or
522 -- enabling the node.
523 -- @param       ...             Virtual path
524 -- @return                      Dispatching tree node
525 function get(...)
526         return _create_node({...})
527 end
528
529 --- Fetch or create a new dispatching node.
530 -- @param       ...             Virtual path
531 -- @return                      Dispatching tree node
532 function node(...)
533         local c = _create_node({...})
534
535         c.module = getfenv(2)._NAME
536         c.auto = nil
537
538         return c
539 end
540
541 function _create_node(path, cache)
542         if #path == 0 then
543                 return context.tree
544         end
545
546         cache = cache or context.treecache
547         local name = table.concat(path, ".")
548         local c = cache[name]
549
550         if not c then
551                 local new = {nodes={}, auto=true, path=util.clone(path)}
552                 local last = table.remove(path)
553
554                 c = _create_node(path, cache)
555
556                 c.nodes[last] = new
557                 cache[name] = new
558
559                 return new
560         else
561                 return c
562         end
563 end
564
565 -- Subdispatchers --
566
567 --- Create a redirect to another dispatching node.
568 -- @param       ...             Virtual path destination
569 function alias(...)
570         local req = {...}
571         return function(...)
572                 for _, r in ipairs({...}) do
573                         req[#req+1] = r
574                 end
575
576                 dispatch(req)
577         end
578 end
579
580 --- Rewrite the first x path values of the request.
581 -- @param       n               Number of path values to replace
582 -- @param       ...             Virtual path to replace removed path values with
583 function rewrite(n, ...)
584         local req = {...}
585         return function(...)
586                 local dispatched = util.clone(context.dispatched)
587
588                 for i=1,n do
589                         table.remove(dispatched, 1)
590                 end
591
592                 for i, r in ipairs(req) do
593                         table.insert(dispatched, i, r)
594                 end
595
596                 for _, r in ipairs({...}) do
597                         dispatched[#dispatched+1] = r
598                 end
599
600                 dispatch(dispatched)
601         end
602 end
603
604
605 local function _call(self, ...)
606         if #self.argv > 0 then
607                 return getfenv()[self.name](unpack(self.argv), ...)
608         else
609                 return getfenv()[self.name](...)
610         end
611 end
612
613 --- Create a function-call dispatching target.
614 -- @param       name    Target function of local controller
615 -- @param       ...             Additional parameters passed to the function
616 function call(name, ...)
617         return {type = "call", argv = {...}, name = name, target = _call}
618 end
619
620
621 local _template = function(self, ...)
622         require "luci.template".render(self.view)
623 end
624
625 --- Create a template render dispatching target.
626 -- @param       name    Template to be rendered
627 function template(name)
628         return {type = "template", view = name, target = _template}
629 end
630
631
632 local function _cbi(self, ...)
633         local cbi = require "luci.cbi"
634         local tpl = require "luci.template"
635         local http = require "luci.http"
636
637         local config = self.config or {}
638         local maps = cbi.load(self.model, ...)
639
640         local state = nil
641
642         for i, res in ipairs(maps) do
643                 res.flow = config
644                 local cstate = res:parse()
645                 if cstate and (not state or cstate < state) then
646                         state = cstate
647                 end
648         end
649
650         if config.on_valid_to and state and state > 0 and state < 2 then
651                 http.redirect(config.on_valid_to)
652                 return
653         end
654
655         if config.on_changed_to and state and state > 1 then
656                 http.redirect(config.on_changed_to)
657                 return
658         end
659
660         if config.on_success_to and state and state > 0 then
661                 http.redirect(config.on_success_to)
662                 return
663         end
664
665         if config.state_handler then
666                 if not config.state_handler(state, maps) then
667                         return
668                 end
669         end
670
671         local pageaction = true
672         http.header("X-CBI-State", state or 0)
673         if not config.noheader then
674                 tpl.render("cbi/header", {state = state})
675         end
676         for i, res in ipairs(maps) do
677                 res:render()
678                 if res.pageaction == false then
679                         pageaction = false
680                 end
681         end
682         if not config.nofooter then
683                 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
684         end
685 end
686
687 --- Create a CBI model dispatching target.
688 -- @param       model   CBI model to be rendered
689 function cbi(model, config)
690         return {type = "cbi", config = config, model = model, target = _cbi}
691 end
692
693
694 local function _arcombine(self, ...)
695         local argv = {...}
696         local target = #argv > 0 and self.targets[2] or self.targets[1]
697         setfenv(target.target, self.env)
698         target:target(unpack(argv))
699 end
700
701 --- Create a combined dispatching target for non argv and argv requests.
702 -- @param trg1  Overview Target
703 -- @param trg2  Detail Target
704 function arcombine(trg1, trg2)
705         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
706 end
707
708
709 local function _form(self, ...)
710         local cbi = require "luci.cbi"
711         local tpl = require "luci.template"
712         local http = require "luci.http"
713
714         local maps = luci.cbi.load(self.model, ...)
715         local state = nil
716
717         for i, res in ipairs(maps) do
718                 local cstate = res:parse()
719                 if cstate and (not state or cstate < state) then
720                         state = cstate
721                 end
722         end
723
724         http.header("X-CBI-State", state or 0)
725         tpl.render("header")
726         for i, res in ipairs(maps) do
727                 res:render()
728         end
729         tpl.render("footer")
730 end
731
732 --- Create a CBI form model dispatching target.
733 -- @param       model   CBI form model tpo be rendered
734 function form(model)
735         return {type = "cbi", model = model, target = _form}
736 end