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