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