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