a80dd706e0575892cf9bd4bc9bb0c1b88296641a
[project/luci.git] / modules / luci-base / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 local fs = require "nixio.fs"
5 local sys = require "luci.sys"
6 local util = require "luci.util"
7 local http = require "luci.http"
8 local nixio = require "nixio", require "nixio.util"
9
10 module("luci.dispatcher", package.seeall)
11 context = util.threadlocal()
12 uci = require "luci.model.uci"
13 i18n = require "luci.i18n"
14 _M.fs = fs
15
16 authenticator = {}
17
18 -- Index table
19 local index = nil
20
21 -- Fastindex
22 local fi
23
24
25 function build_url(...)
26         local path = {...}
27         local url = { http.getenv("SCRIPT_NAME") or "" }
28
29         local k, v
30         for k, v in pairs(context.urltoken) do
31                 url[#url+1] = "/;"
32                 url[#url+1] = http.urlencode(k)
33                 url[#url+1] = "="
34                 url[#url+1] = http.urlencode(v)
35         end
36
37         local p
38         for _, p in ipairs(path) do
39                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
40                         url[#url+1] = "/"
41                         url[#url+1] = p
42                 end
43         end
44
45         return table.concat(url, "")
46 end
47
48 function node_visible(node)
49    if node then
50           return not (
51                  (not node.title or #node.title == 0) or
52                  (not node.target or node.hidden == true) or
53                  (type(node.target) == "table" and node.target.type == "firstchild" and
54                   (type(node.nodes) ~= "table" or not next(node.nodes)))
55           )
56    end
57    return false
58 end
59
60 function node_childs(node)
61         local rv = { }
62         if node then
63                 local k, v
64                 for k, v in util.spairs(node.nodes,
65                         function(a, b)
66                                 return (node.nodes[a].order or 100)
67                                      < (node.nodes[b].order or 100)
68                         end)
69                 do
70                         if node_visible(v) then
71                                 rv[#rv+1] = k
72                         end
73                 end
74         end
75         return rv
76 end
77
78
79 function error404(message)
80         http.status(404, "Not Found")
81         message = message or "Not Found"
82
83         require("luci.template")
84         if not util.copcall(luci.template.render, "error404") then
85                 http.prepare_content("text/plain")
86                 http.write(message)
87         end
88         return false
89 end
90
91 function error500(message)
92         util.perror(message)
93         if not context.template_header_sent then
94                 http.status(500, "Internal Server Error")
95                 http.prepare_content("text/plain")
96                 http.write(message)
97         else
98                 require("luci.template")
99                 if not util.copcall(luci.template.render, "error500", {message=message}) then
100                         http.prepare_content("text/plain")
101                         http.write(message)
102                 end
103         end
104         return false
105 end
106
107 function authenticator.htmlauth(validator, accs, default)
108         local user = http.formvalue("luci_username")
109         local pass = http.formvalue("luci_password")
110
111         if user and validator(user, pass) then
112                 return user
113         end
114
115         if context.urltoken.stok then
116                 context.urltoken.stok = nil
117                 http.header("Set-Cookie", "sysauth=; path="..build_url())
118                 http.redirect(build_url())
119         else
120                 require("luci.i18n")
121                 require("luci.template")
122                 context.path = {}
123                 http.status(403, "Forbidden")
124                 luci.template.render("sysauth", {duser=default, fuser=user})
125         end
126
127         return false
128
129 end
130
131 function httpdispatch(request, prefix)
132         http.context.request = request
133
134         local r = {}
135         context.request = r
136         context.urltoken = {}
137
138         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
139
140         if prefix then
141                 for _, node in ipairs(prefix) do
142                         r[#r+1] = node
143                 end
144         end
145
146         local tokensok = true
147         for node in pathinfo:gmatch("[^/]+") do
148                 local tkey, tval
149                 if tokensok then
150                         tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
151                 end
152                 if tkey then
153                         context.urltoken[tkey] = tval
154                 else
155                         tokensok = false
156                         r[#r+1] = node
157                 end
158         end
159
160         local stat, err = util.coxpcall(function()
161                 dispatch(context.request)
162         end, error500)
163
164         http.close()
165
166         --context._disable_memtrace()
167 end
168
169 function dispatch(request)
170         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
171         local ctx = context
172         ctx.path = request
173
174         local conf = require "luci.config"
175         assert(conf.main,
176                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
177
178         local lang = conf.main.lang or "auto"
179         if lang == "auto" then
180                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
181                 for lpat in aclang:gmatch("[%w-]+") do
182                         lpat = lpat and lpat:gsub("-", "_")
183                         if conf.languages[lpat] then
184                                 lang = lpat
185                                 break
186                         end
187                 end
188         end
189         require "luci.i18n".setlanguage(lang)
190
191         local c = ctx.tree
192         local stat
193         if not c then
194                 c = createtree()
195         end
196
197         local track = {}
198         local args = {}
199         ctx.args = args
200         ctx.requestargs = ctx.requestargs or args
201         local n
202         local token = ctx.urltoken
203         local preq = {}
204         local freq = {}
205
206         for i, s in ipairs(request) do
207                 preq[#preq+1] = s
208                 freq[#freq+1] = s
209                 c = c.nodes[s]
210                 n = i
211                 if not c then
212                         break
213                 end
214
215                 util.update(track, c)
216
217                 if c.leaf then
218                         break
219                 end
220         end
221
222         if c and c.leaf then
223                 for j=n+1, #request do
224                         args[#args+1] = request[j]
225                         freq[#freq+1] = request[j]
226                 end
227         end
228
229         ctx.requestpath = ctx.requestpath or freq
230         ctx.path = preq
231
232         if track.i18n then
233                 i18n.loadc(track.i18n)
234         end
235
236         -- Init template engine
237         if (c and c.index) or not track.notemplate then
238                 local tpl = require("luci.template")
239                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
240                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
241                         media = nil
242                         for name, theme in pairs(luci.config.themes) do
243                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
244                                  "themes/%s/header" % fs.basename(theme)) then
245                                         media = theme
246                                 end
247                         end
248                         assert(media, "No valid theme found")
249                 end
250
251                 local function _ifattr(cond, key, val)
252                         if cond then
253                                 local env = getfenv(3)
254                                 local scope = (type(env.self) == "table") and env.self
255                                 return string.format(
256                                         ' %s="%s"', tostring(key),
257                                         util.pcdata(tostring( val
258                                          or (type(env[key]) ~= "function" and env[key])
259                                          or (scope and type(scope[key]) ~= "function" and scope[key])
260                                          or "" ))
261                                 )
262                         else
263                                 return ''
264                         end
265                 end
266
267                 tpl.context.viewns = setmetatable({
268                    write       = http.write;
269                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
270                    translate   = i18n.translate;
271                    translatef  = i18n.translatef;
272                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
273                    striptags   = util.striptags;
274                    pcdata      = util.pcdata;
275                    media       = media;
276                    theme       = fs.basename(media);
277                    resource    = luci.config.main.resourcebase;
278                    ifattr      = function(...) return _ifattr(...) end;
279                    attr        = function(...) return _ifattr(true, ...) end;
280                 }, {__index=function(table, key)
281                         if key == "controller" then
282                                 return build_url()
283                         elseif key == "REQUEST_URI" then
284                                 return build_url(unpack(ctx.requestpath))
285                         else
286                                 return rawget(table, key) or _G[key]
287                         end
288                 end})
289         end
290
291         track.dependent = (track.dependent ~= false)
292         assert(not track.dependent or not track.auto,
293                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
294                 "has no parent node so the access to this location has been denied.\n" ..
295                 "This is a software bug, please report this message at " ..
296                 "http://luci.subsignal.org/trac/newticket"
297         )
298
299         if track.sysauth then
300                 local authen = type(track.sysauth_authenticator) == "function"
301                  and track.sysauth_authenticator
302                  or authenticator[track.sysauth_authenticator]
303
304                 local def  = (type(track.sysauth) == "string") and track.sysauth
305                 local accs = def and {track.sysauth} or track.sysauth
306                 local sess = ctx.authsession
307                 local verifytoken = false
308                 if not sess then
309                         sess = http.getcookie("sysauth")
310                         sess = sess and sess:match("^[a-f0-9]*$")
311                         verifytoken = true
312                 end
313
314                 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
315                 local user
316
317                 if sdat then
318                         if not verifytoken or ctx.urltoken.stok == sdat.token then
319                                 user = sdat.user
320                         end
321                 else
322                         local eu = http.getenv("HTTP_AUTH_USER")
323                         local ep = http.getenv("HTTP_AUTH_PASS")
324                         if eu and ep and sys.user.checkpasswd(eu, ep) then
325                                 authen = function() return eu end
326                         end
327                 end
328
329                 if not util.contains(accs, user) then
330                         if authen then
331                                 local user, sess = authen(sys.user.checkpasswd, accs, def)
332                                 if not user or not util.contains(accs, user) then
333                                         return
334                                 else
335                                         if not sess then
336                                                 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
337                                                 if sdat then
338                                                         local token = sys.uniqueid(16)
339                                                         util.ubus("session", "set", {
340                                                                 ubus_rpc_session = sdat.ubus_rpc_session,
341                                                                 values = {
342                                                                         user = user,
343                                                                         token = token,
344                                                                         section = sys.uniqueid(16)
345                                                                 }
346                                                         })
347                                                         sess = sdat.ubus_rpc_session
348                                                         ctx.urltoken.stok = token
349                                                 end
350                                         end
351
352                                         if sess then
353                                                 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
354                                                 http.redirect(build_url(unpack(ctx.requestpath)))
355                                                 ctx.authsession = sess
356                                                 ctx.authuser = user
357                                         end
358                                 end
359                         else
360                                 http.status(403, "Forbidden")
361                                 return
362                         end
363                 else
364                         ctx.authsession = sess
365                         ctx.authuser = user
366                 end
367         end
368
369         if track.setgroup then
370                 sys.process.setgroup(track.setgroup)
371         end
372
373         if track.setuser then
374                 -- trigger ubus connection before dropping root privs
375                 util.ubus()
376
377                 sys.process.setuser(track.setuser)
378         end
379
380         local target = nil
381         if c then
382                 if type(c.target) == "function" then
383                         target = c.target
384                 elseif type(c.target) == "table" then
385                         target = c.target.target
386                 end
387         end
388
389         if c and (c.index or type(target) == "function") then
390                 ctx.dispatched = c
391                 ctx.requested = ctx.requested or ctx.dispatched
392         end
393
394         if c and c.index then
395                 local tpl = require "luci.template"
396
397                 if util.copcall(tpl.render, "indexer", {}) then
398                         return true
399                 end
400         end
401
402         if type(target) == "function" then
403                 util.copcall(function()
404                         local oldenv = getfenv(target)
405                         local module = require(c.module)
406                         local env = setmetatable({}, {__index=
407
408                         function(tbl, key)
409                                 return rawget(tbl, key) or module[key] or oldenv[key]
410                         end})
411
412                         setfenv(target, env)
413                 end)
414
415                 local ok, err
416                 if type(c.target) == "table" then
417                         ok, err = util.copcall(target, c.target, unpack(args))
418                 else
419                         ok, err = util.copcall(target, unpack(args))
420                 end
421                 assert(ok,
422                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
423                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
424                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
425         else
426                 local root = node()
427                 if not root or not root.target then
428                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
429                                  "Install luci-mod-admin-full and retry. " ..
430                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
431                 else
432                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
433                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
434                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
435                 end
436         end
437 end
438
439 function createindex()
440         local controllers = { }
441         local base = "%s/controller/" % util.libpath()
442         local _, path
443
444         for path in (fs.glob("%s*.lua" % base) or function() end) do
445                 controllers[#controllers+1] = path
446         end
447
448         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
449                 controllers[#controllers+1] = path
450         end
451
452         if indexcache then
453                 local cachedate = fs.stat(indexcache, "mtime")
454                 if cachedate then
455                         local realdate = 0
456                         for _, obj in ipairs(controllers) do
457                                 local omtime = fs.stat(obj, "mtime")
458                                 realdate = (omtime and omtime > realdate) and omtime or realdate
459                         end
460
461                         if cachedate > realdate and sys.process.info("uid") == 0 then
462                                 assert(
463                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
464                                         and fs.stat(indexcache, "modestr") == "rw-------",
465                                         "Fatal: Indexcache is not sane!"
466                                 )
467
468                                 index = loadfile(indexcache)()
469                                 return index
470                         end
471                 end
472         end
473
474         index = {}
475
476         for _, path in ipairs(controllers) do
477                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
478                 local mod = require(modname)
479                 assert(mod ~= true,
480                        "Invalid controller file found\n" ..
481                        "The file '" .. path .. "' contains an invalid module line.\n" ..
482                        "Please verify whether the module name is set to '" .. modname ..
483                        "' - It must correspond to the file path!")
484
485                 local idx = mod.index
486                 assert(type(idx) == "function",
487                        "Invalid controller file found\n" ..
488                        "The file '" .. path .. "' contains no index() function.\n" ..
489                        "Please make sure that the controller contains a valid " ..
490                        "index function and verify the spelling!")
491
492                 index[modname] = idx
493         end
494
495         if indexcache then
496                 local f = nixio.open(indexcache, "w", 600)
497                 f:writeall(util.get_bytecode(index))
498                 f:close()
499         end
500 end
501
502 -- Build the index before if it does not exist yet.
503 function createtree()
504         if not index then
505                 createindex()
506         end
507
508         local ctx  = context
509         local tree = {nodes={}, inreq=true}
510         local modi = {}
511
512         ctx.treecache = setmetatable({}, {__mode="v"})
513         ctx.tree = tree
514         ctx.modifiers = modi
515
516         -- Load default translation
517         require "luci.i18n".loadc("base")
518
519         local scope = setmetatable({}, {__index = luci.dispatcher})
520
521         for k, v in pairs(index) do
522                 scope._NAME = k
523                 setfenv(v, scope)
524                 v()
525         end
526
527         local function modisort(a,b)
528                 return modi[a].order < modi[b].order
529         end
530
531         for _, v in util.spairs(modi, modisort) do
532                 scope._NAME = v.module
533                 setfenv(v.func, scope)
534                 v.func()
535         end
536
537         return tree
538 end
539
540 function modifier(func, order)
541         context.modifiers[#context.modifiers+1] = {
542                 func = func,
543                 order = order or 0,
544                 module
545                         = getfenv(2)._NAME
546         }
547 end
548
549 function assign(path, clone, title, order)
550         local obj  = node(unpack(path))
551         obj.nodes  = nil
552         obj.module = nil
553
554         obj.title = title
555         obj.order = order
556
557         setmetatable(obj, {__index = _create_node(clone)})
558
559         return obj
560 end
561
562 function entry(path, target, title, order)
563         local c = node(unpack(path))
564
565         c.target = target
566         c.title  = title
567         c.order  = order
568         c.module = getfenv(2)._NAME
569
570         return c
571 end
572
573 -- enabling the node.
574 function get(...)
575         return _create_node({...})
576 end
577
578 function node(...)
579         local c = _create_node({...})
580
581         c.module = getfenv(2)._NAME
582         c.auto = nil
583
584         return c
585 end
586
587 function _create_node(path)
588         if #path == 0 then
589                 return context.tree
590         end
591
592         local name = table.concat(path, ".")
593         local c = context.treecache[name]
594
595         if not c then
596                 local last = table.remove(path)
597                 local parent = _create_node(path)
598
599                 c = {nodes={}, auto=true}
600                 -- the node is "in request" if the request path matches
601                 -- at least up to the length of the node path
602                 if parent.inreq and context.path[#path+1] == last then
603                   c.inreq = true
604                 end
605                 parent.nodes[last] = c
606                 context.treecache[name] = c
607         end
608         return c
609 end
610
611 -- Subdispatchers --
612
613 function _firstchild()
614    local path = { unpack(context.path) }
615    local name = table.concat(path, ".")
616    local node = context.treecache[name]
617
618    local lowest
619    if node and node.nodes and next(node.nodes) then
620           local k, v
621           for k, v in pairs(node.nodes) do
622                  if not lowest or
623                         (v.order or 100) < (node.nodes[lowest].order or 100)
624                  then
625                         lowest = k
626                  end
627           end
628    end
629
630    assert(lowest ~= nil,
631                   "The requested node contains no childs, unable to redispatch")
632
633    path[#path+1] = lowest
634    dispatch(path)
635 end
636
637 function firstchild()
638    return { type = "firstchild", target = _firstchild }
639 end
640
641 function alias(...)
642         local req = {...}
643         return function(...)
644                 for _, r in ipairs({...}) do
645                         req[#req+1] = r
646                 end
647
648                 dispatch(req)
649         end
650 end
651
652 function rewrite(n, ...)
653         local req = {...}
654         return function(...)
655                 local dispatched = util.clone(context.dispatched)
656
657                 for i=1,n do
658                         table.remove(dispatched, 1)
659                 end
660
661                 for i, r in ipairs(req) do
662                         table.insert(dispatched, i, r)
663                 end
664
665                 for _, r in ipairs({...}) do
666                         dispatched[#dispatched+1] = r
667                 end
668
669                 dispatch(dispatched)
670         end
671 end
672
673
674 local function _call(self, ...)
675         local func = getfenv()[self.name]
676         assert(func ~= nil,
677                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
678
679         assert(type(func) == "function",
680                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
681                'of type "' .. type(func) .. '".')
682
683         if #self.argv > 0 then
684                 return func(unpack(self.argv), ...)
685         else
686                 return func(...)
687         end
688 end
689
690 function call(name, ...)
691         return {type = "call", argv = {...}, name = name, target = _call}
692 end
693
694
695 local _template = function(self, ...)
696         require "luci.template".render(self.view)
697 end
698
699 function template(name)
700         return {type = "template", view = name, target = _template}
701 end
702
703
704 local function _cbi(self, ...)
705         local cbi = require "luci.cbi"
706         local tpl = require "luci.template"
707         local http = require "luci.http"
708
709         local config = self.config or {}
710         local maps = cbi.load(self.model, ...)
711
712         local state = nil
713
714         for i, res in ipairs(maps) do
715                 res.flow = config
716                 local cstate = res:parse()
717                 if cstate and (not state or cstate < state) then
718                         state = cstate
719                 end
720         end
721
722         local function _resolve_path(path)
723                 return type(path) == "table" and build_url(unpack(path)) or path
724         end
725
726         if config.on_valid_to and state and state > 0 and state < 2 then
727                 http.redirect(_resolve_path(config.on_valid_to))
728                 return
729         end
730
731         if config.on_changed_to and state and state > 1 then
732                 http.redirect(_resolve_path(config.on_changed_to))
733                 return
734         end
735
736         if config.on_success_to and state and state > 0 then
737                 http.redirect(_resolve_path(config.on_success_to))
738                 return
739         end
740
741         if config.state_handler then
742                 if not config.state_handler(state, maps) then
743                         return
744                 end
745         end
746
747         http.header("X-CBI-State", state or 0)
748
749         if not config.noheader then
750                 tpl.render("cbi/header", {state = state})
751         end
752
753         local redirect
754         local messages
755         local applymap   = false
756         local pageaction = true
757         local parsechain = { }
758
759         for i, res in ipairs(maps) do
760                 if res.apply_needed and res.parsechain then
761                         local c
762                         for _, c in ipairs(res.parsechain) do
763                                 parsechain[#parsechain+1] = c
764                         end
765                         applymap = true
766                 end
767
768                 if res.redirect then
769                         redirect = redirect or res.redirect
770                 end
771
772                 if res.pageaction == false then
773                         pageaction = false
774                 end
775
776                 if res.message then
777                         messages = messages or { }
778                         messages[#messages+1] = res.message
779                 end
780         end
781
782         for i, res in ipairs(maps) do
783                 res:render({
784                         firstmap   = (i == 1),
785                         applymap   = applymap,
786                         redirect   = redirect,
787                         messages   = messages,
788                         pageaction = pageaction,
789                         parsechain = parsechain
790                 })
791         end
792
793         if not config.nofooter then
794                 tpl.render("cbi/footer", {
795                         flow       = config,
796                         pageaction = pageaction,
797                         redirect   = redirect,
798                         state      = state,
799                         autoapply  = config.autoapply
800                 })
801         end
802 end
803
804 function cbi(model, config)
805         return {type = "cbi", config = config, model = model, target = _cbi}
806 end
807
808
809 local function _arcombine(self, ...)
810         local argv = {...}
811         local target = #argv > 0 and self.targets[2] or self.targets[1]
812         setfenv(target.target, self.env)
813         target:target(unpack(argv))
814 end
815
816 function arcombine(trg1, trg2)
817         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
818 end
819
820
821 local function _form(self, ...)
822         local cbi = require "luci.cbi"
823         local tpl = require "luci.template"
824         local http = require "luci.http"
825
826         local maps = luci.cbi.load(self.model, ...)
827         local state = nil
828
829         for i, res in ipairs(maps) do
830                 local cstate = res:parse()
831                 if cstate and (not state or cstate < state) then
832                         state = cstate
833                 end
834         end
835
836         http.header("X-CBI-State", state or 0)
837         tpl.render("header")
838         for i, res in ipairs(maps) do
839                 res:render()
840         end
841         tpl.render("footer")
842 end
843
844 function form(model)
845         return {type = "cbi", model = model, target = _form}
846 end
847
848 translate = i18n.translate
849
850 -- This function does not actually translate the given argument but
851 -- is used by build/i18n-scan.pl to find translatable entries.
852 function _(text)
853         return text
854 end