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