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"
24 function build_url(...)
26 local url = { http.getenv("SCRIPT_NAME") or "" }
29 for _, p in ipairs(path) do
30 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
40 return table.concat(url, "")
43 function node_visible(node)
46 (not node.title or #node.title == 0) or
47 (not node.target or node.hidden == true) or
48 (type(node.target) == "table" and node.target.type == "firstchild" and
49 (type(node.nodes) ~= "table" or not next(node.nodes)))
55 function node_childs(node)
59 for k, v in util.spairs(node.nodes,
61 return (node.nodes[a].order or 100)
62 < (node.nodes[b].order or 100)
65 if node_visible(v) then
74 function error404(message)
75 http.status(404, "Not Found")
76 message = message or "Not Found"
78 require("luci.template")
79 if not util.copcall(luci.template.render, "error404") then
80 http.prepare_content("text/plain")
86 function error500(message)
88 if not context.template_header_sent then
89 http.status(500, "Internal Server Error")
90 http.prepare_content("text/plain")
93 require("luci.template")
94 if not util.copcall(luci.template.render, "error500", {message=message}) then
95 http.prepare_content("text/plain")
102 function httpdispatch(request, prefix)
103 http.context.request = request
108 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
111 for _, node in ipairs(prefix) do
116 for node in pathinfo:gmatch("[^/]+") do
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
126 --context._disable_memtrace()
129 local function require_post_security(target)
130 if type(target) == "table" then
131 if type(target.post) == "table" then
132 local param_name, required_val, request_val
134 for param_name, required_val in pairs(target.post) do
135 request_val = http.formvalue(param_name)
137 if (type(required_val) == "string" and
138 request_val ~= required_val) or
139 (required_val == true and
140 (request_val == nil or request_val == ""))
149 return (target.post == true)
155 function test_post_security()
156 if http.getenv("REQUEST_METHOD") ~= "POST" then
157 http.status(405, "Method Not Allowed")
158 http.header("Allow", "POST")
162 if http.formvalue("token") ~= context.authtoken then
163 http.status(403, "Forbidden")
164 luci.template.render("csrftoken")
171 local function session_retrieve(sid, allowed_users)
172 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
174 if type(sdat) == "table" and
175 type(sdat.values) == "table" and
176 type(sdat.values.token) == "string" and
177 (not allowed_users or
178 util.contains(allowed_users, sdat.values.username))
180 return sid, sdat.values
186 local function session_setup(user, pass, allowed_users)
187 if util.contains(allowed_users, user) then
188 local login = util.ubus("session", "login", {
191 timeout = tonumber(luci.config.sauth.sessiontime)
194 local rp = context.requestpath
195 and table.concat(context.requestpath, "/") or ""
197 if type(login) == "table" and
198 type(login.ubus_rpc_session) == "string"
200 util.ubus("session", "set", {
201 ubus_rpc_session = login.ubus_rpc_session,
202 values = { token = sys.uniqueid(16) }
205 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
206 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
208 return session_retrieve(login.ubus_rpc_session)
211 io.stderr:write("luci: failed login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
218 function dispatch(request)
219 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
223 local conf = require "luci.config"
225 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
227 local i18n = require "luci.i18n"
228 local lang = conf.main.lang or "auto"
229 if lang == "auto" then
230 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
231 for aclang in aclang:gmatch("[%w_-]+") do
232 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
233 if country and culture then
234 local cc = "%s_%s" %{ country, culture:lower() }
235 if conf.languages[cc] then
238 elseif conf.languages[country] then
242 elseif conf.languages[aclang] then
248 if lang == "auto" then
251 i18n.setlanguage(lang)
262 ctx.requestargs = ctx.requestargs or args
267 for i, s in ipairs(request) do
276 util.update(track, c)
284 for j=n+1, #request do
285 args[#args+1] = request[j]
286 freq[#freq+1] = request[j]
290 ctx.requestpath = ctx.requestpath or freq
294 i18n.loadc(track.i18n)
297 -- Init template engine
298 if (c and c.index) or not track.notemplate then
299 local tpl = require("luci.template")
300 local media = track.mediaurlbase or luci.config.main.mediaurlbase
301 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
303 for name, theme in pairs(luci.config.themes) do
304 if name:sub(1,1) ~= "." and pcall(tpl.Template,
305 "themes/%s/header" % fs.basename(theme)) then
309 assert(media, "No valid theme found")
312 local function _ifattr(cond, key, val)
314 local env = getfenv(3)
315 local scope = (type(env.self) == "table") and env.self
316 if type(val) == "table" then
317 if not next(val) then
320 val = util.serialize_json(val)
323 return string.format(
324 ' %s="%s"', tostring(key),
325 util.pcdata(tostring( val
326 or (type(env[key]) ~= "function" and env[key])
327 or (scope and type(scope[key]) ~= "function" and scope[key])
335 tpl.context.viewns = setmetatable({
337 include = function(name) tpl.Template(name):render(getfenv(2)) end;
338 translate = i18n.translate;
339 translatef = i18n.translatef;
340 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
341 striptags = util.striptags;
342 pcdata = util.pcdata;
344 theme = fs.basename(media);
345 resource = luci.config.main.resourcebase;
346 ifattr = function(...) return _ifattr(...) end;
347 attr = function(...) return _ifattr(true, ...) end;
349 }, {__index=function(tbl, key)
350 if key == "controller" then
352 elseif key == "REQUEST_URI" then
353 return build_url(unpack(ctx.requestpath))
354 elseif key == "FULL_REQUEST_URI" then
355 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
356 local query = http.getenv("QUERY_STRING")
357 if query and #query > 0 then
361 return table.concat(url, "")
362 elseif key == "token" then
365 return rawget(tbl, key) or _G[key]
370 track.dependent = (track.dependent ~= false)
371 assert(not track.dependent or not track.auto,
372 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
373 "has no parent node so the access to this location has been denied.\n" ..
374 "This is a software bug, please report this message at " ..
375 "https://github.com/openwrt/luci/issues"
378 if track.sysauth and not ctx.authsession then
379 local authen = track.sysauth_authenticator
380 local _, sid, sdat, default_user, allowed_users
382 if type(authen) == "string" and authen ~= "htmlauth" then
383 error500("Unsupported authenticator %q configured" % authen)
387 if type(track.sysauth) == "table" then
388 default_user, allowed_users = nil, track.sysauth
390 default_user, allowed_users = track.sysauth, { track.sysauth }
393 if type(authen) == "function" then
394 _, sid = authen(sys.user.checkpasswd, allowed_users)
396 sid = http.getcookie("sysauth")
399 sid, sdat = session_retrieve(sid, allowed_users)
401 if not (sid and sdat) and authen == "htmlauth" then
402 local user = http.getenv("HTTP_AUTH_USER")
403 local pass = http.getenv("HTTP_AUTH_PASS")
405 if user == nil and pass == nil then
406 user = http.formvalue("luci_username")
407 pass = http.formvalue("luci_password")
410 sid, sdat = session_setup(user, pass, allowed_users)
413 local tmpl = require "luci.template"
417 http.status(403, "Forbidden")
418 tmpl.render(track.sysauth_template or "sysauth", {
419 duser = default_user,
426 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
427 http.redirect(build_url(unpack(ctx.requestpath)))
430 if not sid or not sdat then
431 http.status(403, "Forbidden")
435 ctx.authsession = sid
436 ctx.authtoken = sdat.token
437 ctx.authuser = sdat.username
440 if c and require_post_security(c.target) then
441 if not test_post_security(c) then
446 if track.setgroup then
447 sys.process.setgroup(track.setgroup)
450 if track.setuser then
451 sys.process.setuser(track.setuser)
456 if type(c.target) == "function" then
458 elseif type(c.target) == "table" then
459 target = c.target.target
463 if c and (c.index or type(target) == "function") then
465 ctx.requested = ctx.requested or ctx.dispatched
468 if c and c.index then
469 local tpl = require "luci.template"
471 if util.copcall(tpl.render, "indexer", {}) then
476 if type(target) == "function" then
477 util.copcall(function()
478 local oldenv = getfenv(target)
479 local module = require(c.module)
480 local env = setmetatable({}, {__index=
483 return rawget(tbl, key) or module[key] or oldenv[key]
490 if type(c.target) == "table" then
491 ok, err = util.copcall(target, c.target, unpack(args))
493 ok, err = util.copcall(target, unpack(args))
496 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
497 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
498 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
501 if not root or not root.target then
502 error404("No root node was registered, this usually happens if no module was installed.\n" ..
503 "Install luci-mod-admin-full and retry. " ..
504 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
506 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
507 "If this url belongs to an extension, make sure it is properly installed.\n" ..
508 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
513 function createindex()
514 local controllers = { }
515 local base = "%s/controller/" % util.libpath()
518 for path in (fs.glob("%s*.lua" % base) or function() end) do
519 controllers[#controllers+1] = path
522 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
523 controllers[#controllers+1] = path
527 local cachedate = fs.stat(indexcache, "mtime")
530 for _, obj in ipairs(controllers) do
531 local omtime = fs.stat(obj, "mtime")
532 realdate = (omtime and omtime > realdate) and omtime or realdate
535 if cachedate > realdate and sys.process.info("uid") == 0 then
537 sys.process.info("uid") == fs.stat(indexcache, "uid")
538 and fs.stat(indexcache, "modestr") == "rw-------",
539 "Fatal: Indexcache is not sane!"
542 index = loadfile(indexcache)()
550 for _, path in ipairs(controllers) do
551 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
552 local mod = require(modname)
554 "Invalid controller file found\n" ..
555 "The file '" .. path .. "' contains an invalid module line.\n" ..
556 "Please verify whether the module name is set to '" .. modname ..
557 "' - It must correspond to the file path!")
559 local idx = mod.index
560 assert(type(idx) == "function",
561 "Invalid controller file found\n" ..
562 "The file '" .. path .. "' contains no index() function.\n" ..
563 "Please make sure that the controller contains a valid " ..
564 "index function and verify the spelling!")
570 local f = nixio.open(indexcache, "w", 600)
571 f:writeall(util.get_bytecode(index))
576 -- Build the index before if it does not exist yet.
577 function createtree()
583 local tree = {nodes={}, inreq=true}
586 ctx.treecache = setmetatable({}, {__mode="v"})
590 -- Load default translation
591 require "luci.i18n".loadc("base")
593 local scope = setmetatable({}, {__index = luci.dispatcher})
595 for k, v in pairs(index) do
601 local function modisort(a,b)
602 return modi[a].order < modi[b].order
605 for _, v in util.spairs(modi, modisort) do
606 scope._NAME = v.module
607 setfenv(v.func, scope)
614 function modifier(func, order)
615 context.modifiers[#context.modifiers+1] = {
623 function assign(path, clone, title, order)
624 local obj = node(unpack(path))
631 setmetatable(obj, {__index = _create_node(clone)})
636 function entry(path, target, title, order)
637 local c = node(unpack(path))
642 c.module = getfenv(2)._NAME
647 -- enabling the node.
649 return _create_node({...})
653 local c = _create_node({...})
655 c.module = getfenv(2)._NAME
662 local i, path = nil, {}
663 for i = 1, select('#', ...) do
664 local name, arg = nil, tostring(select(i, ...))
665 for name in arg:gmatch("[^/]+") do
670 for i = #path, 1, -1 do
671 local node = context.treecache[table.concat(path, ".", 1, i)]
672 if node and (i == #path or node.leaf) then
673 return node, build_url(unpack(path))
678 function _create_node(path)
683 local name = table.concat(path, ".")
684 local c = context.treecache[name]
687 local last = table.remove(path)
688 local parent = _create_node(path)
690 c = {nodes={}, auto=true}
691 -- the node is "in request" if the request path matches
692 -- at least up to the length of the node path
693 if parent.inreq and context.path[#path+1] == last then
696 parent.nodes[last] = c
697 context.treecache[name] = c
704 function _firstchild()
705 local path = { unpack(context.path) }
706 local name = table.concat(path, ".")
707 local node = context.treecache[name]
710 if node and node.nodes and next(node.nodes) then
712 for k, v in pairs(node.nodes) do
714 (v.order or 100) < (node.nodes[lowest].order or 100)
721 assert(lowest ~= nil,
722 "The requested node contains no childs, unable to redispatch")
724 path[#path+1] = lowest
728 function firstchild()
729 return { type = "firstchild", target = _firstchild }
735 for _, r in ipairs({...}) do
743 function rewrite(n, ...)
746 local dispatched = util.clone(context.dispatched)
749 table.remove(dispatched, 1)
752 for i, r in ipairs(req) do
753 table.insert(dispatched, i, r)
756 for _, r in ipairs({...}) do
757 dispatched[#dispatched+1] = r
765 local function _call(self, ...)
766 local func = getfenv()[self.name]
768 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
770 assert(type(func) == "function",
771 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
772 'of type "' .. type(func) .. '".')
774 if #self.argv > 0 then
775 return func(unpack(self.argv), ...)
781 function call(name, ...)
782 return {type = "call", argv = {...}, name = name, target = _call}
785 function post_on(params, name, ...)
796 return post_on(true, ...)
800 local _template = function(self, ...)
801 require "luci.template".render(self.view)
804 function template(name)
805 return {type = "template", view = name, target = _template}
809 local function _cbi(self, ...)
810 local cbi = require "luci.cbi"
811 local tpl = require "luci.template"
812 local http = require "luci.http"
814 local config = self.config or {}
815 local maps = cbi.load(self.model, ...)
819 for i, res in ipairs(maps) do
821 local cstate = res:parse()
822 if cstate and (not state or cstate < state) then
827 local function _resolve_path(path)
828 return type(path) == "table" and build_url(unpack(path)) or path
831 if config.on_valid_to and state and state > 0 and state < 2 then
832 http.redirect(_resolve_path(config.on_valid_to))
836 if config.on_changed_to and state and state > 1 then
837 http.redirect(_resolve_path(config.on_changed_to))
841 if config.on_success_to and state and state > 0 then
842 http.redirect(_resolve_path(config.on_success_to))
846 if config.state_handler then
847 if not config.state_handler(state, maps) then
852 http.header("X-CBI-State", state or 0)
854 if not config.noheader then
855 tpl.render("cbi/header", {state = state})
860 local applymap = false
861 local pageaction = true
862 local parsechain = { }
864 for i, res in ipairs(maps) do
865 if res.apply_needed and res.parsechain then
867 for _, c in ipairs(res.parsechain) do
868 parsechain[#parsechain+1] = c
874 redirect = redirect or res.redirect
877 if res.pageaction == false then
882 messages = messages or { }
883 messages[#messages+1] = res.message
887 for i, res in ipairs(maps) do
893 pageaction = pageaction,
894 parsechain = parsechain
898 if not config.nofooter then
899 tpl.render("cbi/footer", {
901 pageaction = pageaction,
904 autoapply = config.autoapply
909 function cbi(model, config)
912 post = { ["cbi.submit"] = true },
920 local function _arcombine(self, ...)
922 local target = #argv > 0 and self.targets[2] or self.targets[1]
923 setfenv(target.target, self.env)
924 target:target(unpack(argv))
927 function arcombine(trg1, trg2)
928 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
932 local function _form(self, ...)
933 local cbi = require "luci.cbi"
934 local tpl = require "luci.template"
935 local http = require "luci.http"
937 local maps = luci.cbi.load(self.model, ...)
940 for i, res in ipairs(maps) do
941 local cstate = res:parse()
942 if cstate and (not state or cstate < state) then
947 http.header("X-CBI-State", state or 0)
949 for i, res in ipairs(maps) do
958 post = { ["cbi.submit"] = true },
964 translate = i18n.translate
966 -- This function does not actually translate the given argument but
967 -- is used by build/i18n-scan.pl to find translatable entries.