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 _, p in ipairs(path) do
32 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
42 return table.concat(url, "")
45 function node_visible(node)
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)))
57 function node_childs(node)
61 for k, v in util.spairs(node.nodes,
63 return (node.nodes[a].order or 100)
64 < (node.nodes[b].order or 100)
67 if node_visible(v) then
76 function error404(message)
77 http.status(404, "Not Found")
78 message = message or "Not Found"
80 require("luci.template")
81 if not util.copcall(luci.template.render, "error404") then
82 http.prepare_content("text/plain")
88 function error500(message)
90 if not context.template_header_sent then
91 http.status(500, "Internal Server Error")
92 http.prepare_content("text/plain")
95 require("luci.template")
96 if not util.copcall(luci.template.render, "error500", {message=message}) then
97 http.prepare_content("text/plain")
104 function authenticator.htmlauth(validator, accs, default)
105 local user = http.formvalue("luci_username")
106 local pass = http.formvalue("luci_password")
108 if user and validator(user, pass) then
113 require("luci.template")
115 http.status(403, "Forbidden")
116 luci.template.render("sysauth", {duser=default, fuser=user})
122 function httpdispatch(request, prefix)
123 http.context.request = request
128 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
131 for _, node in ipairs(prefix) do
136 for node in pathinfo:gmatch("[^/]+") do
140 local stat, err = util.coxpcall(function()
141 dispatch(context.request)
146 --context._disable_memtrace()
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
154 for param_name, required_val in pairs(target.post) do
155 request_val = http.formvalue(param_name)
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 == ""))
169 return (target.post == true)
175 function dispatch(request)
176 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
180 local conf = require "luci.config"
182 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
184 local lang = conf.main.lang or "auto"
185 if lang == "auto" then
186 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
187 for lpat in aclang:gmatch("[%w-]+") do
188 lpat = lpat and lpat:gsub("-", "_")
189 if conf.languages[lpat] then
195 require "luci.i18n".setlanguage(lang)
206 ctx.requestargs = ctx.requestargs or args
211 for i, s in ipairs(request) do
220 util.update(track, c)
228 for j=n+1, #request do
229 args[#args+1] = request[j]
230 freq[#freq+1] = request[j]
234 ctx.requestpath = ctx.requestpath or freq
238 i18n.loadc(track.i18n)
241 -- Init template engine
242 if (c and c.index) or not track.notemplate then
243 local tpl = require("luci.template")
244 local media = track.mediaurlbase or luci.config.main.mediaurlbase
245 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
247 for name, theme in pairs(luci.config.themes) do
248 if name:sub(1,1) ~= "." and pcall(tpl.Template,
249 "themes/%s/header" % fs.basename(theme)) then
253 assert(media, "No valid theme found")
256 local function _ifattr(cond, key, val)
258 local env = getfenv(3)
259 local scope = (type(env.self) == "table") and env.self
260 return string.format(
261 ' %s="%s"', tostring(key),
262 util.pcdata(tostring( val
263 or (type(env[key]) ~= "function" and env[key])
264 or (scope and type(scope[key]) ~= "function" and scope[key])
272 tpl.context.viewns = setmetatable({
274 include = function(name) tpl.Template(name):render(getfenv(2)) end;
275 translate = i18n.translate;
276 translatef = i18n.translatef;
277 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
278 striptags = util.striptags;
279 pcdata = util.pcdata;
281 theme = fs.basename(media);
282 resource = luci.config.main.resourcebase;
283 ifattr = function(...) return _ifattr(...) end;
284 attr = function(...) return _ifattr(true, ...) end;
286 }, {__index=function(table, key)
287 if key == "controller" then
289 elseif key == "REQUEST_URI" then
290 return build_url(unpack(ctx.requestpath))
291 elseif key == "token" then
294 return rawget(table, key) or _G[key]
299 track.dependent = (track.dependent ~= false)
300 assert(not track.dependent or not track.auto,
301 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
302 "has no parent node so the access to this location has been denied.\n" ..
303 "This is a software bug, please report this message at " ..
304 "http://luci.subsignal.org/trac/newticket"
307 if track.sysauth then
308 local authen = type(track.sysauth_authenticator) == "function"
309 and track.sysauth_authenticator
310 or authenticator[track.sysauth_authenticator]
312 local def = (type(track.sysauth) == "string") and track.sysauth
313 local accs = def and {track.sysauth} or track.sysauth
314 local sess = ctx.authsession
316 sess = http.getcookie("sysauth")
317 sess = sess and sess:match("^[a-f0-9]*$")
320 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
327 local eu = http.getenv("HTTP_AUTH_USER")
328 local ep = http.getenv("HTTP_AUTH_PASS")
329 if eu and ep and sys.user.checkpasswd(eu, ep) then
330 authen = function() return eu end
334 if not util.contains(accs, user) then
336 local user, sess = authen(sys.user.checkpasswd, accs, def)
338 if not user or not util.contains(accs, user) then
342 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
344 token = sys.uniqueid(16)
345 util.ubus("session", "set", {
346 ubus_rpc_session = sdat.ubus_rpc_session,
350 section = sys.uniqueid(16)
353 sess = sdat.ubus_rpc_session
357 if sess and token then
358 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
360 ctx.authsession = sess
361 ctx.authtoken = token
364 http.redirect(build_url(unpack(ctx.requestpath)))
368 http.status(403, "Forbidden")
372 ctx.authsession = sess
373 ctx.authtoken = token
378 if c and require_post_security(c.target) then
379 if http.getenv("REQUEST_METHOD") ~= "POST" then
380 http.status(405, "Method Not Allowed")
381 http.header("Allow", "POST")
385 if http.formvalue("token") ~= ctx.authtoken then
386 http.status(403, "Forbidden")
387 luci.template.render("csrftoken")
392 if track.setgroup then
393 sys.process.setgroup(track.setgroup)
396 if track.setuser then
397 -- trigger ubus connection before dropping root privs
400 sys.process.setuser(track.setuser)
405 if type(c.target) == "function" then
407 elseif type(c.target) == "table" then
408 target = c.target.target
412 if c and (c.index or type(target) == "function") then
414 ctx.requested = ctx.requested or ctx.dispatched
417 if c and c.index then
418 local tpl = require "luci.template"
420 if util.copcall(tpl.render, "indexer", {}) then
425 if type(target) == "function" then
426 util.copcall(function()
427 local oldenv = getfenv(target)
428 local module = require(c.module)
429 local env = setmetatable({}, {__index=
432 return rawget(tbl, key) or module[key] or oldenv[key]
439 if type(c.target) == "table" then
440 ok, err = util.copcall(target, c.target, unpack(args))
442 ok, err = util.copcall(target, unpack(args))
445 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
446 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
447 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
450 if not root or not root.target then
451 error404("No root node was registered, this usually happens if no module was installed.\n" ..
452 "Install luci-mod-admin-full and retry. " ..
453 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
455 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
456 "If this url belongs to an extension, make sure it is properly installed.\n" ..
457 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
462 function createindex()
463 local controllers = { }
464 local base = "%s/controller/" % util.libpath()
467 for path in (fs.glob("%s*.lua" % base) or function() end) do
468 controllers[#controllers+1] = path
471 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
472 controllers[#controllers+1] = path
476 local cachedate = fs.stat(indexcache, "mtime")
479 for _, obj in ipairs(controllers) do
480 local omtime = fs.stat(obj, "mtime")
481 realdate = (omtime and omtime > realdate) and omtime or realdate
484 if cachedate > realdate and sys.process.info("uid") == 0 then
486 sys.process.info("uid") == fs.stat(indexcache, "uid")
487 and fs.stat(indexcache, "modestr") == "rw-------",
488 "Fatal: Indexcache is not sane!"
491 index = loadfile(indexcache)()
499 for _, path in ipairs(controllers) do
500 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
501 local mod = require(modname)
503 "Invalid controller file found\n" ..
504 "The file '" .. path .. "' contains an invalid module line.\n" ..
505 "Please verify whether the module name is set to '" .. modname ..
506 "' - It must correspond to the file path!")
508 local idx = mod.index
509 assert(type(idx) == "function",
510 "Invalid controller file found\n" ..
511 "The file '" .. path .. "' contains no index() function.\n" ..
512 "Please make sure that the controller contains a valid " ..
513 "index function and verify the spelling!")
519 local f = nixio.open(indexcache, "w", 600)
520 f:writeall(util.get_bytecode(index))
525 -- Build the index before if it does not exist yet.
526 function createtree()
532 local tree = {nodes={}, inreq=true}
535 ctx.treecache = setmetatable({}, {__mode="v"})
539 -- Load default translation
540 require "luci.i18n".loadc("base")
542 local scope = setmetatable({}, {__index = luci.dispatcher})
544 for k, v in pairs(index) do
550 local function modisort(a,b)
551 return modi[a].order < modi[b].order
554 for _, v in util.spairs(modi, modisort) do
555 scope._NAME = v.module
556 setfenv(v.func, scope)
563 function modifier(func, order)
564 context.modifiers[#context.modifiers+1] = {
572 function assign(path, clone, title, order)
573 local obj = node(unpack(path))
580 setmetatable(obj, {__index = _create_node(clone)})
585 function entry(path, target, title, order)
586 local c = node(unpack(path))
591 c.module = getfenv(2)._NAME
596 -- enabling the node.
598 return _create_node({...})
602 local c = _create_node({...})
604 c.module = getfenv(2)._NAME
610 function _create_node(path)
615 local name = table.concat(path, ".")
616 local c = context.treecache[name]
619 local last = table.remove(path)
620 local parent = _create_node(path)
622 c = {nodes={}, auto=true}
623 -- the node is "in request" if the request path matches
624 -- at least up to the length of the node path
625 if parent.inreq and context.path[#path+1] == last then
628 parent.nodes[last] = c
629 context.treecache[name] = c
636 function _firstchild()
637 local path = { unpack(context.path) }
638 local name = table.concat(path, ".")
639 local node = context.treecache[name]
642 if node and node.nodes and next(node.nodes) then
644 for k, v in pairs(node.nodes) do
646 (v.order or 100) < (node.nodes[lowest].order or 100)
653 assert(lowest ~= nil,
654 "The requested node contains no childs, unable to redispatch")
656 path[#path+1] = lowest
660 function firstchild()
661 return { type = "firstchild", target = _firstchild }
667 for _, r in ipairs({...}) do
675 function rewrite(n, ...)
678 local dispatched = util.clone(context.dispatched)
681 table.remove(dispatched, 1)
684 for i, r in ipairs(req) do
685 table.insert(dispatched, i, r)
688 for _, r in ipairs({...}) do
689 dispatched[#dispatched+1] = r
697 local function _call(self, ...)
698 local func = getfenv()[self.name]
700 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
702 assert(type(func) == "function",
703 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
704 'of type "' .. type(func) .. '".')
706 if #self.argv > 0 then
707 return func(unpack(self.argv), ...)
713 function call(name, ...)
714 return {type = "call", argv = {...}, name = name, target = _call}
717 function post_on(params, name, ...)
728 return post_on(true, ...)
732 local _template = function(self, ...)
733 require "luci.template".render(self.view)
736 function template(name)
737 return {type = "template", view = name, target = _template}
741 local function _cbi(self, ...)
742 local cbi = require "luci.cbi"
743 local tpl = require "luci.template"
744 local http = require "luci.http"
746 local config = self.config or {}
747 local maps = cbi.load(self.model, ...)
751 for i, res in ipairs(maps) do
753 local cstate = res:parse()
754 if cstate and (not state or cstate < state) then
759 local function _resolve_path(path)
760 return type(path) == "table" and build_url(unpack(path)) or path
763 if config.on_valid_to and state and state > 0 and state < 2 then
764 http.redirect(_resolve_path(config.on_valid_to))
768 if config.on_changed_to and state and state > 1 then
769 http.redirect(_resolve_path(config.on_changed_to))
773 if config.on_success_to and state and state > 0 then
774 http.redirect(_resolve_path(config.on_success_to))
778 if config.state_handler then
779 if not config.state_handler(state, maps) then
784 http.header("X-CBI-State", state or 0)
786 if not config.noheader then
787 tpl.render("cbi/header", {state = state})
792 local applymap = false
793 local pageaction = true
794 local parsechain = { }
796 for i, res in ipairs(maps) do
797 if res.apply_needed and res.parsechain then
799 for _, c in ipairs(res.parsechain) do
800 parsechain[#parsechain+1] = c
806 redirect = redirect or res.redirect
809 if res.pageaction == false then
814 messages = messages or { }
815 messages[#messages+1] = res.message
819 for i, res in ipairs(maps) do
825 pageaction = pageaction,
826 parsechain = parsechain
830 if not config.nofooter then
831 tpl.render("cbi/footer", {
833 pageaction = pageaction,
836 autoapply = config.autoapply
841 function cbi(model, config)
844 post = { ["cbi.submit"] = "1" },
852 local function _arcombine(self, ...)
854 local target = #argv > 0 and self.targets[2] or self.targets[1]
855 setfenv(target.target, self.env)
856 target:target(unpack(argv))
859 function arcombine(trg1, trg2)
860 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
864 local function _form(self, ...)
865 local cbi = require "luci.cbi"
866 local tpl = require "luci.template"
867 local http = require "luci.http"
869 local maps = luci.cbi.load(self.model, ...)
872 for i, res in ipairs(maps) do
873 local cstate = res:parse()
874 if cstate and (not state or cstate < state) then
879 http.header("X-CBI-State", state or 0)
881 for i, res in ipairs(maps) do
890 post = { ["cbi.submit"] = "1" },
896 translate = i18n.translate
898 -- This function does not actually translate the given argument but
899 -- is used by build/i18n-scan.pl to find translatable entries.