Merge pull request #302 from chris5560/master
[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                 sys.process.setuser(track.setuser)
375         end
376
377         local target = nil
378         if c then
379                 if type(c.target) == "function" then
380                         target = c.target
381                 elseif type(c.target) == "table" then
382                         target = c.target.target
383                 end
384         end
385
386         if c and (c.index or type(target) == "function") then
387                 ctx.dispatched = c
388                 ctx.requested = ctx.requested or ctx.dispatched
389         end
390
391         if c and c.index then
392                 local tpl = require "luci.template"
393
394                 if util.copcall(tpl.render, "indexer", {}) then
395                         return true
396                 end
397         end
398
399         if type(target) == "function" then
400                 util.copcall(function()
401                         local oldenv = getfenv(target)
402                         local module = require(c.module)
403                         local env = setmetatable({}, {__index=
404
405                         function(tbl, key)
406                                 return rawget(tbl, key) or module[key] or oldenv[key]
407                         end})
408
409                         setfenv(target, env)
410                 end)
411
412                 local ok, err
413                 if type(c.target) == "table" then
414                         ok, err = util.copcall(target, c.target, unpack(args))
415                 else
416                         ok, err = util.copcall(target, unpack(args))
417                 end
418                 assert(ok,
419                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
420                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
421                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
422         else
423                 local root = node()
424                 if not root or not root.target then
425                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
426                                  "Install luci-mod-admin-full and retry. " ..
427                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
428                 else
429                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
430                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
431                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
432                 end
433         end
434 end
435
436 function createindex()
437         local controllers = { }
438         local base = "%s/controller/" % util.libpath()
439         local _, path
440
441         for path in (fs.glob("%s*.lua" % base) or function() end) do
442                 controllers[#controllers+1] = path
443         end
444
445         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
446                 controllers[#controllers+1] = path
447         end
448
449         if indexcache then
450                 local cachedate = fs.stat(indexcache, "mtime")
451                 if cachedate then
452                         local realdate = 0
453                         for _, obj in ipairs(controllers) do
454                                 local omtime = fs.stat(obj, "mtime")
455                                 realdate = (omtime and omtime > realdate) and omtime or realdate
456                         end
457
458                         if cachedate > realdate and sys.process.info("uid") == 0 then
459                                 assert(
460                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
461                                         and fs.stat(indexcache, "modestr") == "rw-------",
462                                         "Fatal: Indexcache is not sane!"
463                                 )
464
465                                 index = loadfile(indexcache)()
466                                 return index
467                         end
468                 end
469         end
470
471         index = {}
472
473         for _, path in ipairs(controllers) do
474                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
475                 local mod = require(modname)
476                 assert(mod ~= true,
477                        "Invalid controller file found\n" ..
478                        "The file '" .. path .. "' contains an invalid module line.\n" ..
479                        "Please verify whether the module name is set to '" .. modname ..
480                        "' - It must correspond to the file path!")
481
482                 local idx = mod.index
483                 assert(type(idx) == "function",
484                        "Invalid controller file found\n" ..
485                        "The file '" .. path .. "' contains no index() function.\n" ..
486                        "Please make sure that the controller contains a valid " ..
487                        "index function and verify the spelling!")
488
489                 index[modname] = idx
490         end
491
492         if indexcache then
493                 local f = nixio.open(indexcache, "w", 600)
494                 f:writeall(util.get_bytecode(index))
495                 f:close()
496         end
497 end
498
499 -- Build the index before if it does not exist yet.
500 function createtree()
501         if not index then
502                 createindex()
503         end
504
505         local ctx  = context
506         local tree = {nodes={}, inreq=true}
507         local modi = {}
508
509         ctx.treecache = setmetatable({}, {__mode="v"})
510         ctx.tree = tree
511         ctx.modifiers = modi
512
513         -- Load default translation
514         require "luci.i18n".loadc("base")
515
516         local scope = setmetatable({}, {__index = luci.dispatcher})
517
518         for k, v in pairs(index) do
519                 scope._NAME = k
520                 setfenv(v, scope)
521                 v()
522         end
523
524         local function modisort(a,b)
525                 return modi[a].order < modi[b].order
526         end
527
528         for _, v in util.spairs(modi, modisort) do
529                 scope._NAME = v.module
530                 setfenv(v.func, scope)
531                 v.func()
532         end
533
534         return tree
535 end
536
537 function modifier(func, order)
538         context.modifiers[#context.modifiers+1] = {
539                 func = func,
540                 order = order or 0,
541                 module
542                         = getfenv(2)._NAME
543         }
544 end
545
546 function assign(path, clone, title, order)
547         local obj  = node(unpack(path))
548         obj.nodes  = nil
549         obj.module = nil
550
551         obj.title = title
552         obj.order = order
553
554         setmetatable(obj, {__index = _create_node(clone)})
555
556         return obj
557 end
558
559 function entry(path, target, title, order)
560         local c = node(unpack(path))
561
562         c.target = target
563         c.title  = title
564         c.order  = order
565         c.module = getfenv(2)._NAME
566
567         return c
568 end
569
570 -- enabling the node.
571 function get(...)
572         return _create_node({...})
573 end
574
575 function node(...)
576         local c = _create_node({...})
577
578         c.module = getfenv(2)._NAME
579         c.auto = nil
580
581         return c
582 end
583
584 function _create_node(path)
585         if #path == 0 then
586                 return context.tree
587         end
588
589         local name = table.concat(path, ".")
590         local c = context.treecache[name]
591
592         if not c then
593                 local last = table.remove(path)
594                 local parent = _create_node(path)
595
596                 c = {nodes={}, auto=true}
597                 -- the node is "in request" if the request path matches
598                 -- at least up to the length of the node path
599                 if parent.inreq and context.path[#path+1] == last then
600                   c.inreq = true
601                 end
602                 parent.nodes[last] = c
603                 context.treecache[name] = c
604         end
605         return c
606 end
607
608 -- Subdispatchers --
609
610 function _firstchild()
611    local path = { unpack(context.path) }
612    local name = table.concat(path, ".")
613    local node = context.treecache[name]
614
615    local lowest
616    if node and node.nodes and next(node.nodes) then
617           local k, v
618           for k, v in pairs(node.nodes) do
619                  if not lowest or
620                         (v.order or 100) < (node.nodes[lowest].order or 100)
621                  then
622                         lowest = k
623                  end
624           end
625    end
626
627    assert(lowest ~= nil,
628                   "The requested node contains no childs, unable to redispatch")
629
630    path[#path+1] = lowest
631    dispatch(path)
632 end
633
634 function firstchild()
635    return { type = "firstchild", target = _firstchild }
636 end
637
638 function alias(...)
639         local req = {...}
640         return function(...)
641                 for _, r in ipairs({...}) do
642                         req[#req+1] = r
643                 end
644
645                 dispatch(req)
646         end
647 end
648
649 function rewrite(n, ...)
650         local req = {...}
651         return function(...)
652                 local dispatched = util.clone(context.dispatched)
653
654                 for i=1,n do
655                         table.remove(dispatched, 1)
656                 end
657
658                 for i, r in ipairs(req) do
659                         table.insert(dispatched, i, r)
660                 end
661
662                 for _, r in ipairs({...}) do
663                         dispatched[#dispatched+1] = r
664                 end
665
666                 dispatch(dispatched)
667         end
668 end
669
670
671 local function _call(self, ...)
672         local func = getfenv()[self.name]
673         assert(func ~= nil,
674                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
675
676         assert(type(func) == "function",
677                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
678                'of type "' .. type(func) .. '".')
679
680         if #self.argv > 0 then
681                 return func(unpack(self.argv), ...)
682         else
683                 return func(...)
684         end
685 end
686
687 function call(name, ...)
688         return {type = "call", argv = {...}, name = name, target = _call}
689 end
690
691
692 local _template = function(self, ...)
693         require "luci.template".render(self.view)
694 end
695
696 function template(name)
697         return {type = "template", view = name, target = _template}
698 end
699
700
701 local function _cbi(self, ...)
702         local cbi = require "luci.cbi"
703         local tpl = require "luci.template"
704         local http = require "luci.http"
705
706         local config = self.config or {}
707         local maps = cbi.load(self.model, ...)
708
709         local state = nil
710
711         for i, res in ipairs(maps) do
712                 res.flow = config
713                 local cstate = res:parse()
714                 if cstate and (not state or cstate < state) then
715                         state = cstate
716                 end
717         end
718
719         local function _resolve_path(path)
720                 return type(path) == "table" and build_url(unpack(path)) or path
721         end
722
723         if config.on_valid_to and state and state > 0 and state < 2 then
724                 http.redirect(_resolve_path(config.on_valid_to))
725                 return
726         end
727
728         if config.on_changed_to and state and state > 1 then
729                 http.redirect(_resolve_path(config.on_changed_to))
730                 return
731         end
732
733         if config.on_success_to and state and state > 0 then
734                 http.redirect(_resolve_path(config.on_success_to))
735                 return
736         end
737
738         if config.state_handler then
739                 if not config.state_handler(state, maps) then
740                         return
741                 end
742         end
743
744         http.header("X-CBI-State", state or 0)
745
746         if not config.noheader then
747                 tpl.render("cbi/header", {state = state})
748         end
749
750         local redirect
751         local messages
752         local applymap   = false
753         local pageaction = true
754         local parsechain = { }
755
756         for i, res in ipairs(maps) do
757                 if res.apply_needed and res.parsechain then
758                         local c
759                         for _, c in ipairs(res.parsechain) do
760                                 parsechain[#parsechain+1] = c
761                         end
762                         applymap = true
763                 end
764
765                 if res.redirect then
766                         redirect = redirect or res.redirect
767                 end
768
769                 if res.pageaction == false then
770                         pageaction = false
771                 end
772
773                 if res.message then
774                         messages = messages or { }
775                         messages[#messages+1] = res.message
776                 end
777         end
778
779         for i, res in ipairs(maps) do
780                 res:render({
781                         firstmap   = (i == 1),
782                         applymap   = applymap,
783                         redirect   = redirect,
784                         messages   = messages,
785                         pageaction = pageaction,
786                         parsechain = parsechain
787                 })
788         end
789
790         if not config.nofooter then
791                 tpl.render("cbi/footer", {
792                         flow       = config,
793                         pageaction = pageaction,
794                         redirect   = redirect,
795                         state      = state,
796                         autoapply  = config.autoapply
797                 })
798         end
799 end
800
801 function cbi(model, config)
802         return {type = "cbi", config = config, model = model, target = _cbi}
803 end
804
805
806 local function _arcombine(self, ...)
807         local argv = {...}
808         local target = #argv > 0 and self.targets[2] or self.targets[1]
809         setfenv(target.target, self.env)
810         target:target(unpack(argv))
811 end
812
813 function arcombine(trg1, trg2)
814         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
815 end
816
817
818 local function _form(self, ...)
819         local cbi = require "luci.cbi"
820         local tpl = require "luci.template"
821         local http = require "luci.http"
822
823         local maps = luci.cbi.load(self.model, ...)
824         local state = nil
825
826         for i, res in ipairs(maps) do
827                 local cstate = res:parse()
828                 if cstate and (not state or cstate < state) then
829                         state = cstate
830                 end
831         end
832
833         http.header("X-CBI-State", state or 0)
834         tpl.render("header")
835         for i, res in ipairs(maps) do
836                 res:render()
837         end
838         tpl.render("footer")
839 end
840
841 function form(model)
842         return {type = "cbi", model = model, target = _form}
843 end
844
845 translate = i18n.translate
846
847 -- This function does not actually translate the given argument but
848 -- is used by build/i18n-scan.pl to find translatable entries.
849 function _(text)
850         return text
851 end