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;
289 }, {__index=function(table, key)
290 if key == "controller" then
292 elseif key == "REQUEST_URI" then
293 return build_url(unpack(ctx.requestpath))
295 return rawget(table, key) or _G[key]
300 track.dependent = (track.dependent ~= false)
301 assert(not track.dependent or not track.auto,
302 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
303 "has no parent node so the access to this location has been denied.\n" ..
304 "This is a software bug, please report this message at " ..
305 "http://luci.subsignal.org/trac/newticket"
308 if track.sysauth then
309 local authen = type(track.sysauth_authenticator) == "function"
310 and track.sysauth_authenticator
311 or authenticator[track.sysauth_authenticator]
313 local def = (type(track.sysauth) == "string") and track.sysauth
314 local accs = def and {track.sysauth} or track.sysauth
315 local sess = ctx.authsession
316 local verifytoken = false
318 sess = http.getcookie("sysauth")
319 sess = sess and sess:match("^[a-f0-9]*$")
323 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
327 if not verifytoken or ctx.urltoken.stok == sdat.token then
331 local eu = http.getenv("HTTP_AUTH_USER")
332 local ep = http.getenv("HTTP_AUTH_PASS")
333 if eu and ep and sys.user.checkpasswd(eu, ep) then
334 authen = function() return eu end
338 if not util.contains(accs, user) then
340 local user, sess = authen(sys.user.checkpasswd, accs, def)
342 if not user or not util.contains(accs, user) then
346 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
348 token = sys.uniqueid(16)
349 util.ubus("session", "set", {
350 ubus_rpc_session = sdat.ubus_rpc_session,
354 section = sys.uniqueid(16)
357 sess = sdat.ubus_rpc_session
361 if sess and token then
362 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
366 ctx.urltoken.stok = token
367 ctx.authsession = sess
370 http.redirect(build_url(unpack(ctx.requestpath)))
374 http.status(403, "Forbidden")
378 ctx.authsession = sess
383 if c and type(c.target) == "table" and c.target.post == true then
384 if http.getenv("REQUEST_METHOD") ~= "POST" then
385 http.status(405, "Method Not Allowed")
386 http.header("Allow", "POST")
390 if http.formvalue("token") ~= ctx.urltoken.stok then
391 http.status(403, "Forbidden")
392 luci.template.render("csrftoken")
397 if track.setgroup then
398 sys.process.setgroup(track.setgroup)
401 if track.setuser then
402 -- trigger ubus connection before dropping root privs
405 sys.process.setuser(track.setuser)
410 if type(c.target) == "function" then
412 elseif type(c.target) == "table" then
413 target = c.target.target
417 if c and (c.index or type(target) == "function") then
419 ctx.requested = ctx.requested or ctx.dispatched
422 if c and c.index then
423 local tpl = require "luci.template"
425 if util.copcall(tpl.render, "indexer", {}) then
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=
437 return rawget(tbl, key) or module[key] or oldenv[key]
444 if type(c.target) == "table" then
445 ok, err = util.copcall(target, c.target, unpack(args))
447 ok, err = util.copcall(target, unpack(args))
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)"))
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.")
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.")
467 function createindex()
468 local controllers = { }
469 local base = "%s/controller/" % util.libpath()
472 for path in (fs.glob("%s*.lua" % base) or function() end) do
473 controllers[#controllers+1] = path
476 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
477 controllers[#controllers+1] = path
481 local cachedate = fs.stat(indexcache, "mtime")
484 for _, obj in ipairs(controllers) do
485 local omtime = fs.stat(obj, "mtime")
486 realdate = (omtime and omtime > realdate) and omtime or realdate
489 if cachedate > realdate and sys.process.info("uid") == 0 then
491 sys.process.info("uid") == fs.stat(indexcache, "uid")
492 and fs.stat(indexcache, "modestr") == "rw-------",
493 "Fatal: Indexcache is not sane!"
496 index = loadfile(indexcache)()
504 for _, path in ipairs(controllers) do
505 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
506 local mod = require(modname)
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!")
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!")
524 local f = nixio.open(indexcache, "w", 600)
525 f:writeall(util.get_bytecode(index))
530 -- Build the index before if it does not exist yet.
531 function createtree()
537 local tree = {nodes={}, inreq=true}
540 ctx.treecache = setmetatable({}, {__mode="v"})
544 -- Load default translation
545 require "luci.i18n".loadc("base")
547 local scope = setmetatable({}, {__index = luci.dispatcher})
549 for k, v in pairs(index) do
555 local function modisort(a,b)
556 return modi[a].order < modi[b].order
559 for _, v in util.spairs(modi, modisort) do
560 scope._NAME = v.module
561 setfenv(v.func, scope)
568 function modifier(func, order)
569 context.modifiers[#context.modifiers+1] = {
577 function assign(path, clone, title, order)
578 local obj = node(unpack(path))
585 setmetatable(obj, {__index = _create_node(clone)})
590 function entry(path, target, title, order)
591 local c = node(unpack(path))
596 c.module = getfenv(2)._NAME
601 -- enabling the node.
603 return _create_node({...})
607 local c = _create_node({...})
609 c.module = getfenv(2)._NAME
615 function _create_node(path)
620 local name = table.concat(path, ".")
621 local c = context.treecache[name]
624 local last = table.remove(path)
625 local parent = _create_node(path)
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
633 parent.nodes[last] = c
634 context.treecache[name] = c
641 function _firstchild()
642 local path = { unpack(context.path) }
643 local name = table.concat(path, ".")
644 local node = context.treecache[name]
647 if node and node.nodes and next(node.nodes) then
649 for k, v in pairs(node.nodes) do
651 (v.order or 100) < (node.nodes[lowest].order or 100)
658 assert(lowest ~= nil,
659 "The requested node contains no childs, unable to redispatch")
661 path[#path+1] = lowest
665 function firstchild()
666 return { type = "firstchild", target = _firstchild }
672 for _, r in ipairs({...}) do
680 function rewrite(n, ...)
683 local dispatched = util.clone(context.dispatched)
686 table.remove(dispatched, 1)
689 for i, r in ipairs(req) do
690 table.insert(dispatched, i, r)
693 for _, r in ipairs({...}) do
694 dispatched[#dispatched+1] = r
702 local function _call(self, ...)
703 local func = getfenv()[self.name]
705 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
707 assert(type(func) == "function",
708 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
709 'of type "' .. type(func) .. '".')
711 if #self.argv > 0 then
712 return func(unpack(self.argv), ...)
718 function call(name, ...)
719 return {type = "call", argv = {...}, name = name, target = _call}
722 function post(name, ...)
733 local _template = function(self, ...)
734 require "luci.template".render(self.view)
737 function template(name)
738 return {type = "template", view = name, target = _template}
742 local function _cbi(self, ...)
743 local cbi = require "luci.cbi"
744 local tpl = require "luci.template"
745 local http = require "luci.http"
746 local disp = require "luci.dispatcher"
748 if http.formvalue("cbi.submit") == "1" and
749 http.formvalue("token") ~= disp.context.urltoken.stok
751 http.status(403, "Forbidden")
752 luci.template.render("csrftoken")
756 local config = self.config or {}
757 local maps = cbi.load(self.model, ...)
761 for i, res in ipairs(maps) do
763 local cstate = res:parse()
764 if cstate and (not state or cstate < state) then
769 local function _resolve_path(path)
770 return type(path) == "table" and build_url(unpack(path)) or path
773 if config.on_valid_to and state and state > 0 and state < 2 then
774 http.redirect(_resolve_path(config.on_valid_to))
778 if config.on_changed_to and state and state > 1 then
779 http.redirect(_resolve_path(config.on_changed_to))
783 if config.on_success_to and state and state > 0 then
784 http.redirect(_resolve_path(config.on_success_to))
788 if config.state_handler then
789 if not config.state_handler(state, maps) then
794 http.header("X-CBI-State", state or 0)
796 if not config.noheader then
797 tpl.render("cbi/header", {state = state})
802 local applymap = false
803 local pageaction = true
804 local parsechain = { }
806 for i, res in ipairs(maps) do
807 if res.apply_needed and res.parsechain then
809 for _, c in ipairs(res.parsechain) do
810 parsechain[#parsechain+1] = c
816 redirect = redirect or res.redirect
819 if res.pageaction == false then
824 messages = messages or { }
825 messages[#messages+1] = res.message
829 for i, res in ipairs(maps) do
835 pageaction = pageaction,
836 parsechain = parsechain
840 if not config.nofooter then
841 tpl.render("cbi/footer", {
843 pageaction = pageaction,
846 autoapply = config.autoapply
851 function cbi(model, config)
852 return {type = "cbi", config = config, model = model, target = _cbi}
856 local function _arcombine(self, ...)
858 local target = #argv > 0 and self.targets[2] or self.targets[1]
859 setfenv(target.target, self.env)
860 target:target(unpack(argv))
863 function arcombine(trg1, trg2)
864 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
868 local function _form(self, ...)
869 local cbi = require "luci.cbi"
870 local tpl = require "luci.template"
871 local http = require "luci.http"
872 local disp = require "luci.dispatcher"
874 if http.formvalue("cbi.submit") == "1" and
875 http.formvalue("token") ~= disp.context.urltoken.stok
877 http.status(403, "Forbidden")
878 luci.template.render("csrftoken")
882 local maps = luci.cbi.load(self.model, ...)
885 for i, res in ipairs(maps) do
886 local cstate = res:parse()
887 if cstate and (not state or cstate < state) then
892 http.header("X-CBI-State", state or 0)
894 for i, res in ipairs(maps) do
901 return {type = "cbi", model = model, target = _form}
904 translate = i18n.translate
906 -- This function does not actually translate the given argument but
907 -- is used by build/i18n-scan.pl to find translatable entries.