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