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