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 _ordered_children(node)
44 local name, child, children = nil, nil, {}
46 for name, child in pairs(node.nodes) do
47 children[#children+1] = {
50 order = child.order or 100
54 table.sort(children, function(a, b)
55 if a.order == b.order then
56 return a.name < b.name
58 return a.order < b.order
65 function node_visible(node)
68 (not node.title or #node.title == 0) or
69 (not node.target or node.hidden == true) or
70 (type(node.target) == "table" and node.target.type == "firstchild" and
71 (type(node.nodes) ~= "table" or not next(node.nodes)))
77 function node_childs(node)
81 for _, child in ipairs(_ordered_children(node)) do
82 if node_visible(child.node) then
83 rv[#rv+1] = child.name
91 function error404(message)
92 http.status(404, "Not Found")
93 message = message or "Not Found"
95 local function render()
96 local template = require "luci.template"
97 template.render("error404")
100 if not util.copcall(render) then
101 http.prepare_content("text/plain")
108 function error500(message)
110 if not context.template_header_sent then
111 http.status(500, "Internal Server Error")
112 http.prepare_content("text/plain")
115 require("luci.template")
116 if not util.copcall(luci.template.render, "error500", {message=message}) then
117 http.prepare_content("text/plain")
124 function httpdispatch(request, prefix)
125 http.context.request = request
130 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
133 for _, node in ipairs(prefix) do
139 for node in pathinfo:gmatch("[^/%z]+") do
143 local stat, err = util.coxpcall(function()
144 dispatch(context.request)
149 --context._disable_memtrace()
152 local function require_post_security(target)
153 if type(target) == "table" then
154 if type(target.post) == "table" then
155 local param_name, required_val, request_val
157 for param_name, required_val in pairs(target.post) do
158 request_val = http.formvalue(param_name)
160 if (type(required_val) == "string" and
161 request_val ~= required_val) or
162 (required_val == true and request_val == nil)
171 return (target.post == true)
177 function test_post_security()
178 if http.getenv("REQUEST_METHOD") ~= "POST" then
179 http.status(405, "Method Not Allowed")
180 http.header("Allow", "POST")
184 if http.formvalue("token") ~= context.authtoken then
185 http.status(403, "Forbidden")
186 luci.template.render("csrftoken")
193 local function session_retrieve(sid, allowed_users)
194 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
196 if type(sdat) == "table" and
197 type(sdat.values) == "table" and
198 type(sdat.values.token) == "string" and
199 (not allowed_users or
200 util.contains(allowed_users, sdat.values.username))
202 uci:set_session_id(sid)
203 return sid, sdat.values
209 local function session_setup(user, pass, allowed_users)
210 if util.contains(allowed_users, user) then
211 local login = util.ubus("session", "login", {
214 timeout = tonumber(luci.config.sauth.sessiontime)
217 local rp = context.requestpath
218 and table.concat(context.requestpath, "/") or ""
220 if type(login) == "table" and
221 type(login.ubus_rpc_session) == "string"
223 util.ubus("session", "set", {
224 ubus_rpc_session = login.ubus_rpc_session,
225 values = { token = sys.uniqueid(16) }
228 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
229 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
231 return session_retrieve(login.ubus_rpc_session)
234 io.stderr:write("luci: failed login on /%s for %s from %s\n"
235 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
241 function dispatch(request)
242 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
246 local conf = require "luci.config"
248 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
250 local i18n = require "luci.i18n"
251 local lang = conf.main.lang or "auto"
252 if lang == "auto" then
253 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
254 for aclang in aclang:gmatch("[%w_-]+") do
255 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
256 if country and culture then
257 local cc = "%s_%s" %{ country, culture:lower() }
258 if conf.languages[cc] then
261 elseif conf.languages[country] then
265 elseif conf.languages[aclang] then
271 if lang == "auto" then
274 i18n.setlanguage(lang)
285 ctx.requestargs = ctx.requestargs or args
290 for i, s in ipairs(request) do
299 util.update(track, c)
307 for j=n+1, #request do
308 args[#args+1] = request[j]
309 freq[#freq+1] = request[j]
313 ctx.requestpath = ctx.requestpath or freq
316 -- Init template engine
317 if (c and c.index) or not track.notemplate then
318 local tpl = require("luci.template")
319 local media = track.mediaurlbase or luci.config.main.mediaurlbase
320 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
322 for name, theme in pairs(luci.config.themes) do
323 if name:sub(1,1) ~= "." and pcall(tpl.Template,
324 "themes/%s/header" % fs.basename(theme)) then
328 assert(media, "No valid theme found")
331 local function _ifattr(cond, key, val, noescape)
333 local env = getfenv(3)
334 local scope = (type(env.self) == "table") and env.self
335 if type(val) == "table" then
336 if not next(val) then
339 val = util.serialize_json(val)
343 val = tostring(val or
344 (type(env[key]) ~= "function" and env[key]) or
345 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
347 if noescape ~= true then
348 val = util.pcdata(val)
351 return string.format(' %s="%s"', tostring(key), val)
357 tpl.context.viewns = setmetatable({
359 include = function(name) tpl.Template(name):render(getfenv(2)) end;
360 translate = i18n.translate;
361 translatef = i18n.translatef;
362 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
363 striptags = util.striptags;
364 pcdata = util.pcdata;
366 theme = fs.basename(media);
367 resource = luci.config.main.resourcebase;
368 ifattr = function(...) return _ifattr(...) end;
369 attr = function(...) return _ifattr(true, ...) end;
371 }, {__index=function(tbl, key)
372 if key == "controller" then
374 elseif key == "REQUEST_URI" then
375 return build_url(unpack(ctx.requestpath))
376 elseif key == "FULL_REQUEST_URI" then
377 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
378 local query = http.getenv("QUERY_STRING")
379 if query and #query > 0 then
383 return table.concat(url, "")
384 elseif key == "token" then
387 return rawget(tbl, key) or _G[key]
392 track.dependent = (track.dependent ~= false)
393 assert(not track.dependent or not track.auto,
394 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
395 "has no parent node so the access to this location has been denied.\n" ..
396 "This is a software bug, please report this message at " ..
397 "https://github.com/openwrt/luci/issues"
400 if track.sysauth and not ctx.authsession then
401 local authen = track.sysauth_authenticator
402 local _, sid, sdat, default_user, allowed_users
404 if type(authen) == "string" and authen ~= "htmlauth" then
405 error500("Unsupported authenticator %q configured" % authen)
409 if type(track.sysauth) == "table" then
410 default_user, allowed_users = nil, track.sysauth
412 default_user, allowed_users = track.sysauth, { track.sysauth }
415 if type(authen) == "function" then
416 _, sid = authen(sys.user.checkpasswd, allowed_users)
418 sid = http.getcookie("sysauth")
421 sid, sdat = session_retrieve(sid, allowed_users)
423 if not (sid and sdat) and authen == "htmlauth" then
424 local user = http.getenv("HTTP_AUTH_USER")
425 local pass = http.getenv("HTTP_AUTH_PASS")
427 if user == nil and pass == nil then
428 user = http.formvalue("luci_username")
429 pass = http.formvalue("luci_password")
432 sid, sdat = session_setup(user, pass, allowed_users)
435 local tmpl = require "luci.template"
439 http.status(403, "Forbidden")
440 http.header("X-LuCI-Login-Required", "yes")
441 tmpl.render(track.sysauth_template or "sysauth", {
442 duser = default_user,
449 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
450 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
452 http.redirect(build_url(unpack(ctx.requestpath)))
455 if not sid or not sdat then
456 http.status(403, "Forbidden")
457 http.header("X-LuCI-Login-Required", "yes")
461 ctx.authsession = sid
462 ctx.authtoken = sdat.token
463 ctx.authuser = sdat.username
466 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
467 luci.http.status(200, "OK")
468 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
469 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
473 if c and require_post_security(c.target) then
474 if not test_post_security(c) then
479 if track.setgroup then
480 sys.process.setgroup(track.setgroup)
483 if track.setuser then
484 sys.process.setuser(track.setuser)
489 if type(c.target) == "function" then
491 elseif type(c.target) == "table" then
492 target = c.target.target
496 if c and (c.index or type(target) == "function") then
498 ctx.requested = ctx.requested or ctx.dispatched
501 if c and c.index then
502 local tpl = require "luci.template"
504 if util.copcall(tpl.render, "indexer", {}) then
509 if type(target) == "function" then
510 util.copcall(function()
511 local oldenv = getfenv(target)
512 local module = require(c.module)
513 local env = setmetatable({}, {__index=
516 return rawget(tbl, key) or module[key] or oldenv[key]
523 if type(c.target) == "table" then
524 ok, err = util.copcall(target, c.target, unpack(args))
526 ok, err = util.copcall(target, unpack(args))
529 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
530 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
531 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
535 if not root or not root.target then
536 error404("No root node was registered, this usually happens if no module was installed.\n" ..
537 "Install luci-mod-admin-full and retry. " ..
538 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
540 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
541 "If this url belongs to an extension, make sure it is properly installed.\n" ..
542 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
547 function createindex()
548 local controllers = { }
549 local base = "%s/controller/" % util.libpath()
552 for path in (fs.glob("%s*.lua" % base) or function() end) do
553 controllers[#controllers+1] = path
556 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
557 controllers[#controllers+1] = path
561 local cachedate = fs.stat(indexcache, "mtime")
564 for _, obj in ipairs(controllers) do
565 local omtime = fs.stat(obj, "mtime")
566 realdate = (omtime and omtime > realdate) and omtime or realdate
569 if cachedate > realdate and sys.process.info("uid") == 0 then
571 sys.process.info("uid") == fs.stat(indexcache, "uid")
572 and fs.stat(indexcache, "modestr") == "rw-------",
573 "Fatal: Indexcache is not sane!"
576 index = loadfile(indexcache)()
584 for _, path in ipairs(controllers) do
585 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
586 local mod = require(modname)
588 "Invalid controller file found\n" ..
589 "The file '" .. path .. "' contains an invalid module line.\n" ..
590 "Please verify whether the module name is set to '" .. modname ..
591 "' - It must correspond to the file path!")
593 local idx = mod.index
594 assert(type(idx) == "function",
595 "Invalid controller file found\n" ..
596 "The file '" .. path .. "' contains no index() function.\n" ..
597 "Please make sure that the controller contains a valid " ..
598 "index function and verify the spelling!")
604 local f = nixio.open(indexcache, "w", 600)
605 f:writeall(util.get_bytecode(index))
610 -- Build the index before if it does not exist yet.
611 function createtree()
617 local tree = {nodes={}, inreq=true}
619 ctx.treecache = setmetatable({}, {__mode="v"})
622 local scope = setmetatable({}, {__index = luci.dispatcher})
624 for k, v in pairs(index) do
633 function assign(path, clone, title, order)
634 local obj = node(unpack(path))
641 setmetatable(obj, {__index = _create_node(clone)})
646 function entry(path, target, title, order)
647 local c = node(unpack(path))
652 c.module = getfenv(2)._NAME
657 -- enabling the node.
659 return _create_node({...})
663 local c = _create_node({...})
665 c.module = getfenv(2)._NAME
672 local i, path = nil, {}
673 for i = 1, select('#', ...) do
674 local name, arg = nil, tostring(select(i, ...))
675 for name in arg:gmatch("[^/]+") do
680 for i = #path, 1, -1 do
681 local node = context.treecache[table.concat(path, ".", 1, i)]
682 if node and (i == #path or node.leaf) then
683 return node, build_url(unpack(path))
688 function _create_node(path)
693 local name = table.concat(path, ".")
694 local c = context.treecache[name]
697 local last = table.remove(path)
698 local parent = _create_node(path)
700 c = {nodes={}, auto=true, inreq=true}
703 for _, n in ipairs(path) do
704 if context.path[_] ~= n then
710 c.inreq = c.inreq and (context.path[#path + 1] == last)
712 parent.nodes[last] = c
713 context.treecache[name] = c
721 function _find_eligible_node(root, prefix, deep, types, descend)
722 local children = _ordered_children(root)
724 if not root.leaf and deep ~= nil then
725 local sub_path = { unpack(prefix) }
727 if deep == false then
732 for _, child in ipairs(children) do
733 sub_path[#prefix+1] = child.name
735 local res_path = _find_eligible_node(child.node, sub_path,
746 (type(root.target) == "table" and
747 util.contains(types, root.target.type)))
753 function _find_node(recurse, types)
754 local path = { unpack(context.path) }
755 local name = table.concat(path, ".")
756 local node = context.treecache[name]
758 path = _find_eligible_node(node, path, recurse, types)
763 require "luci.template".render("empty_node_placeholder")
767 function _firstchild()
768 return _find_node(false, nil)
771 function firstchild()
772 return { type = "firstchild", target = _firstchild }
775 function _firstnode()
776 return _find_node(true, { "cbi", "form", "template", "arcombine" })
780 return { type = "firstnode", target = _firstnode }
786 for _, r in ipairs({...}) do
794 function rewrite(n, ...)
797 local dispatched = util.clone(context.dispatched)
800 table.remove(dispatched, 1)
803 for i, r in ipairs(req) do
804 table.insert(dispatched, i, r)
807 for _, r in ipairs({...}) do
808 dispatched[#dispatched+1] = r
816 local function _call(self, ...)
817 local func = getfenv()[self.name]
819 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
821 assert(type(func) == "function",
822 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
823 'of type "' .. type(func) .. '".')
825 if #self.argv > 0 then
826 return func(unpack(self.argv), ...)
832 function call(name, ...)
833 return {type = "call", argv = {...}, name = name, target = _call}
836 function post_on(params, name, ...)
847 return post_on(true, ...)
851 local _template = function(self, ...)
852 require "luci.template".render(self.view)
855 function template(name)
856 return {type = "template", view = name, target = _template}
860 local _view = function(self, ...)
861 require "luci.template".render("view", { view = self.view })
865 return {type = "view", view = name, target = _view}
869 local function _cbi(self, ...)
870 local cbi = require "luci.cbi"
871 local tpl = require "luci.template"
872 local http = require "luci.http"
874 local config = self.config or {}
875 local maps = cbi.load(self.model, ...)
880 for i, res in ipairs(maps) do
881 if util.instanceof(res, cbi.SimpleForm) then
882 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
885 io.stderr:write("please change %s to use the form() action instead.\n"
886 % table.concat(context.request, "/"))
890 local cstate = res:parse()
891 if cstate and (not state or cstate < state) then
896 local function _resolve_path(path)
897 return type(path) == "table" and build_url(unpack(path)) or path
900 if config.on_valid_to and state and state > 0 and state < 2 then
901 http.redirect(_resolve_path(config.on_valid_to))
905 if config.on_changed_to and state and state > 1 then
906 http.redirect(_resolve_path(config.on_changed_to))
910 if config.on_success_to and state and state > 0 then
911 http.redirect(_resolve_path(config.on_success_to))
915 if config.state_handler then
916 if not config.state_handler(state, maps) then
921 http.header("X-CBI-State", state or 0)
923 if not config.noheader then
924 tpl.render("cbi/header", {state = state})
929 local applymap = false
930 local pageaction = true
931 local parsechain = { }
933 for i, res in ipairs(maps) do
934 if res.apply_needed and res.parsechain then
936 for _, c in ipairs(res.parsechain) do
937 parsechain[#parsechain+1] = c
943 redirect = redirect or res.redirect
946 if res.pageaction == false then
951 messages = messages or { }
952 messages[#messages+1] = res.message
956 for i, res in ipairs(maps) do
961 pageaction = pageaction,
962 parsechain = parsechain
966 if not config.nofooter then
967 tpl.render("cbi/footer", {
969 pageaction = pageaction,
972 autoapply = config.autoapply,
973 trigger_apply = applymap
978 function cbi(model, config)
981 post = { ["cbi.submit"] = true },
989 local function _arcombine(self, ...)
991 local target = #argv > 0 and self.targets[2] or self.targets[1]
992 setfenv(target.target, self.env)
993 target:target(unpack(argv))
996 function arcombine(trg1, trg2)
997 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1001 local function _form(self, ...)
1002 local cbi = require "luci.cbi"
1003 local tpl = require "luci.template"
1004 local http = require "luci.http"
1006 local maps = luci.cbi.load(self.model, ...)
1010 for i, res in ipairs(maps) do
1011 local cstate = res:parse()
1012 if cstate and (not state or cstate < state) then
1017 http.header("X-CBI-State", state or 0)
1018 tpl.render("header")
1019 for i, res in ipairs(maps) do
1022 tpl.render("footer")
1025 function form(model)
1028 post = { ["cbi.submit"] = true },
1034 translate = i18n.translate
1036 -- This function does not actually translate the given argument but
1037 -- is used by build/i18n-scan.pl to find translatable entries.