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.
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"
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
26 function build_url(...)
28 local url = { http.getenv("SCRIPT_NAME") or "" }
31 for k, v in pairs(context.urltoken) do
33 url[#url+1] = http.urlencode(k)
35 url[#url+1] = http.urlencode(v)
39 for _, p in ipairs(path) do
40 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
46 return table.concat(url, "")
49 function node_visible(node)
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)))
61 function node_childs(node)
65 for k, v in util.spairs(node.nodes,
67 return (node.nodes[a].order or 100)
68 < (node.nodes[b].order or 100)
71 if node_visible(v) then
80 function error404(message)
81 http.status(404, "Not Found")
82 message = message or "Not Found"
84 require("luci.template")
85 if not util.copcall(luci.template.render, "error404") then
86 http.prepare_content("text/plain")
92 function error500(message)
94 if not context.template_header_sent then
95 http.status(500, "Internal Server Error")
96 http.prepare_content("text/plain")
99 require("luci.template")
100 if not util.copcall(luci.template.render, "error500", {message=message}) then
101 http.prepare_content("text/plain")
108 function authenticator.htmlauth(validator, accs, default)
109 local user = http.formvalue("luci_username")
110 local pass = http.formvalue("luci_password")
112 if user and validator(user, pass) then
116 if context.urltoken.stok then
117 context.urltoken.stok = nil
119 local cookie = 'sysauth=%s; expires=%s; path=%s/' %{
120 http.getcookie('sysauth') or 'x',
121 'Thu, 01 Jan 1970 01:00:00 GMT',
125 http.header("Set-Cookie", cookie)
126 http.redirect(build_url())
129 require("luci.template")
131 http.status(403, "Forbidden")
132 luci.template.render("sysauth", {duser=default, fuser=user})
139 function httpdispatch(request, prefix)
140 http.context.request = request
144 context.urltoken = {}
146 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
149 for _, node in ipairs(prefix) do
154 local tokensok = true
155 for node in pathinfo:gmatch("[^/]+") do
158 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
161 context.urltoken[tkey] = tval
168 local stat, err = util.coxpcall(function()
169 dispatch(context.request)
174 --context._disable_memtrace()
177 function dispatch(request)
178 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
182 local conf = require "luci.config"
184 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
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
197 require "luci.i18n".setlanguage(lang)
208 ctx.requestargs = ctx.requestargs or args
210 local token = ctx.urltoken
214 for i, s in ipairs(request) do
223 util.update(track, c)
231 for j=n+1, #request do
232 args[#args+1] = request[j]
233 freq[#freq+1] = request[j]
237 ctx.requestpath = ctx.requestpath or freq
241 i18n.loadc(track.i18n)
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
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
256 assert(media, "No valid theme found")
259 local function _ifattr(cond, key, val)
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])
275 tpl.context.viewns = setmetatable({
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;
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;
290 }, {__index=function(table, key)
291 if key == "controller" then
293 elseif key == "REQUEST_URI" then
294 return build_url(unpack(ctx.requestpath))
296 return rawget(table, key) or _G[key]
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"
309 if track.sysauth then
310 local authen = type(track.sysauth_authenticator) == "function"
311 and track.sysauth_authenticator
312 or authenticator[track.sysauth_authenticator]
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
319 sess = http.getcookie("sysauth")
320 sess = sess and sess:match("^[a-f0-9]*$")
324 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
328 if not verifytoken or ctx.urltoken.stok == sdat.token then
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
339 if not util.contains(accs, user) then
341 local user, sess = authen(sys.user.checkpasswd, accs, def)
343 if not user or not util.contains(accs, user) then
347 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
349 token = sys.uniqueid(16)
350 util.ubus("session", "set", {
351 ubus_rpc_session = sdat.ubus_rpc_session,
355 section = sys.uniqueid(16)
358 sess = sdat.ubus_rpc_session
362 if sess and token then
363 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
367 ctx.urltoken.stok = token
368 ctx.authsession = sess
371 http.redirect(build_url(unpack(ctx.requestpath)))
375 http.status(403, "Forbidden")
379 ctx.authsession = sess
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")
391 if http.formvalue("token") ~= ctx.urltoken.stok then
392 http.status(403, "Forbidden")
393 luci.template.render("csrftoken")
398 if track.setgroup then
399 sys.process.setgroup(track.setgroup)
402 if track.setuser then
403 -- trigger ubus connection before dropping root privs
406 sys.process.setuser(track.setuser)
411 if type(c.target) == "function" then
413 elseif type(c.target) == "table" then
414 target = c.target.target
418 if c and (c.index or type(target) == "function") then
420 ctx.requested = ctx.requested or ctx.dispatched
423 if c and c.index then
424 local tpl = require "luci.template"
426 if util.copcall(tpl.render, "indexer", {}) then
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=
438 return rawget(tbl, key) or module[key] or oldenv[key]
445 if type(c.target) == "table" then
446 ok, err = util.copcall(target, c.target, unpack(args))
448 ok, err = util.copcall(target, unpack(args))
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)"))
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.")
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.")
468 function createindex()
469 local controllers = { }
470 local base = "%s/controller/" % util.libpath()
473 for path in (fs.glob("%s*.lua" % base) or function() end) do
474 controllers[#controllers+1] = path
477 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
478 controllers[#controllers+1] = path
482 local cachedate = fs.stat(indexcache, "mtime")
485 for _, obj in ipairs(controllers) do
486 local omtime = fs.stat(obj, "mtime")
487 realdate = (omtime and omtime > realdate) and omtime or realdate
490 if cachedate > realdate and sys.process.info("uid") == 0 then
492 sys.process.info("uid") == fs.stat(indexcache, "uid")
493 and fs.stat(indexcache, "modestr") == "rw-------",
494 "Fatal: Indexcache is not sane!"
497 index = loadfile(indexcache)()
505 for _, path in ipairs(controllers) do
506 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
507 local mod = require(modname)
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!")
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!")
525 local f = nixio.open(indexcache, "w", 600)
526 f:writeall(util.get_bytecode(index))
531 -- Build the index before if it does not exist yet.
532 function createtree()
538 local tree = {nodes={}, inreq=true}
541 ctx.treecache = setmetatable({}, {__mode="v"})
545 -- Load default translation
546 require "luci.i18n".loadc("base")
548 local scope = setmetatable({}, {__index = luci.dispatcher})
550 for k, v in pairs(index) do
556 local function modisort(a,b)
557 return modi[a].order < modi[b].order
560 for _, v in util.spairs(modi, modisort) do
561 scope._NAME = v.module
562 setfenv(v.func, scope)
569 function modifier(func, order)
570 context.modifiers[#context.modifiers+1] = {
578 function assign(path, clone, title, order)
579 local obj = node(unpack(path))
586 setmetatable(obj, {__index = _create_node(clone)})
591 function entry(path, target, title, order)
592 local c = node(unpack(path))
597 c.module = getfenv(2)._NAME
602 -- enabling the node.
604 return _create_node({...})
608 local c = _create_node({...})
610 c.module = getfenv(2)._NAME
616 function _create_node(path)
621 local name = table.concat(path, ".")
622 local c = context.treecache[name]
625 local last = table.remove(path)
626 local parent = _create_node(path)
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
634 parent.nodes[last] = c
635 context.treecache[name] = c
642 function _firstchild()
643 local path = { unpack(context.path) }
644 local name = table.concat(path, ".")
645 local node = context.treecache[name]
648 if node and node.nodes and next(node.nodes) then
650 for k, v in pairs(node.nodes) do
652 (v.order or 100) < (node.nodes[lowest].order or 100)
659 assert(lowest ~= nil,
660 "The requested node contains no childs, unable to redispatch")
662 path[#path+1] = lowest
666 function firstchild()
667 return { type = "firstchild", target = _firstchild }
673 for _, r in ipairs({...}) do
681 function rewrite(n, ...)
684 local dispatched = util.clone(context.dispatched)
687 table.remove(dispatched, 1)
690 for i, r in ipairs(req) do
691 table.insert(dispatched, i, r)
694 for _, r in ipairs({...}) do
695 dispatched[#dispatched+1] = r
703 local function _call(self, ...)
704 local func = getfenv()[self.name]
706 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
708 assert(type(func) == "function",
709 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
710 'of type "' .. type(func) .. '".')
712 if #self.argv > 0 then
713 return func(unpack(self.argv), ...)
719 function call(name, ...)
720 return {type = "call", argv = {...}, name = name, target = _call}
723 function post(name, ...)
734 local _template = function(self, ...)
735 require "luci.template".render(self.view)
738 function template(name)
739 return {type = "template", view = name, target = _template}
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"
749 if http.formvalue("cbi.submit") == "1" and
750 http.formvalue("token") ~= disp.context.urltoken.stok
752 http.status(403, "Forbidden")
753 luci.template.render("csrftoken")
757 local config = self.config or {}
758 local maps = cbi.load(self.model, ...)
762 for i, res in ipairs(maps) do
764 local cstate = res:parse()
765 if cstate and (not state or cstate < state) then
770 local function _resolve_path(path)
771 return type(path) == "table" and build_url(unpack(path)) or path
774 if config.on_valid_to and state and state > 0 and state < 2 then
775 http.redirect(_resolve_path(config.on_valid_to))
779 if config.on_changed_to and state and state > 1 then
780 http.redirect(_resolve_path(config.on_changed_to))
784 if config.on_success_to and state and state > 0 then
785 http.redirect(_resolve_path(config.on_success_to))
789 if config.state_handler then
790 if not config.state_handler(state, maps) then
795 http.header("X-CBI-State", state or 0)
797 if not config.noheader then
798 tpl.render("cbi/header", {state = state})
803 local applymap = false
804 local pageaction = true
805 local parsechain = { }
807 for i, res in ipairs(maps) do
808 if res.apply_needed and res.parsechain then
810 for _, c in ipairs(res.parsechain) do
811 parsechain[#parsechain+1] = c
817 redirect = redirect or res.redirect
820 if res.pageaction == false then
825 messages = messages or { }
826 messages[#messages+1] = res.message
830 for i, res in ipairs(maps) do
836 pageaction = pageaction,
837 parsechain = parsechain
841 if not config.nofooter then
842 tpl.render("cbi/footer", {
844 pageaction = pageaction,
847 autoapply = config.autoapply
852 function cbi(model, config)
853 return {type = "cbi", config = config, model = model, target = _cbi}
857 local function _arcombine(self, ...)
859 local target = #argv > 0 and self.targets[2] or self.targets[1]
860 setfenv(target.target, self.env)
861 target:target(unpack(argv))
864 function arcombine(trg1, trg2)
865 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
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"
875 if http.formvalue("cbi.submit") == "1" and
876 http.formvalue("token") ~= disp.context.urltoken.stok
878 http.status(403, "Forbidden")
879 luci.template.render("csrftoken")
883 local maps = luci.cbi.load(self.model, ...)
886 for i, res in ipairs(maps) do
887 local cstate = res:parse()
888 if cstate and (not state or cstate < state) then
893 http.header("X-CBI-State", state or 0)
895 for i, res in ipairs(maps) do
902 return {type = "cbi", model = model, target = _form}
905 translate = i18n.translate
907 -- This function does not actually translate the given argument but
908 -- is used by build/i18n-scan.pl to find translatable entries.