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