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