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, args)
153 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
154 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
157 if type(target) == "table" then
158 if type(target.post) == "table" then
159 local param_name, required_val, request_val
161 for param_name, required_val in pairs(target.post) do
162 request_val = http.formvalue(param_name)
164 if (type(required_val) == "string" and
165 request_val ~= required_val) or
166 (required_val == true and request_val == nil)
175 return (target.post == true)
181 function test_post_security()
182 if http.getenv("REQUEST_METHOD") ~= "POST" then
183 http.status(405, "Method Not Allowed")
184 http.header("Allow", "POST")
188 if http.formvalue("token") ~= context.authtoken then
189 http.status(403, "Forbidden")
190 luci.template.render("csrftoken")
197 local function session_retrieve(sid, allowed_users)
198 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
200 if type(sdat) == "table" and
201 type(sdat.values) == "table" and
202 type(sdat.values.token) == "string" and
203 (not allowed_users or
204 util.contains(allowed_users, sdat.values.username))
206 uci:set_session_id(sid)
207 return sid, sdat.values
213 local function session_setup(user, pass, allowed_users)
214 if util.contains(allowed_users, user) then
215 local login = util.ubus("session", "login", {
218 timeout = tonumber(luci.config.sauth.sessiontime)
221 local rp = context.requestpath
222 and table.concat(context.requestpath, "/") or ""
224 if type(login) == "table" and
225 type(login.ubus_rpc_session) == "string"
227 util.ubus("session", "set", {
228 ubus_rpc_session = login.ubus_rpc_session,
229 values = { token = sys.uniqueid(16) }
232 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
233 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
235 return session_retrieve(login.ubus_rpc_session)
238 io.stderr:write("luci: failed login on /%s for %s from %s\n"
239 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
245 function dispatch(request)
246 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
250 local conf = require "luci.config"
252 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
254 local i18n = require "luci.i18n"
255 local lang = conf.main.lang or "auto"
256 if lang == "auto" then
257 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
258 for aclang in aclang:gmatch("[%w_-]+") do
259 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
260 if country and culture then
261 local cc = "%s_%s" %{ country, culture:lower() }
262 if conf.languages[cc] then
265 elseif conf.languages[country] then
269 elseif conf.languages[aclang] then
275 if lang == "auto" then
278 i18n.setlanguage(lang)
289 ctx.requestargs = ctx.requestargs or args
294 for i, s in ipairs(request) do
303 util.update(track, c)
311 for j=n+1, #request do
312 args[#args+1] = request[j]
313 freq[#freq+1] = request[j]
317 ctx.requestpath = ctx.requestpath or freq
320 -- Init template engine
321 if (c and c.index) or not track.notemplate then
322 local tpl = require("luci.template")
323 local media = track.mediaurlbase or luci.config.main.mediaurlbase
324 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
326 for name, theme in pairs(luci.config.themes) do
327 if name:sub(1,1) ~= "." and pcall(tpl.Template,
328 "themes/%s/header" % fs.basename(theme)) then
332 assert(media, "No valid theme found")
335 local function _ifattr(cond, key, val, noescape)
337 local env = getfenv(3)
338 local scope = (type(env.self) == "table") and env.self
339 if type(val) == "table" then
340 if not next(val) then
343 val = util.serialize_json(val)
347 val = tostring(val or
348 (type(env[key]) ~= "function" and env[key]) or
349 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
351 if noescape ~= true then
352 val = util.pcdata(val)
355 return string.format(' %s="%s"', tostring(key), val)
361 tpl.context.viewns = setmetatable({
363 include = function(name) tpl.Template(name):render(getfenv(2)) end;
364 translate = i18n.translate;
365 translatef = i18n.translatef;
366 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
367 striptags = util.striptags;
368 pcdata = util.pcdata;
370 theme = fs.basename(media);
371 resource = luci.config.main.resourcebase;
372 ifattr = function(...) return _ifattr(...) end;
373 attr = function(...) return _ifattr(true, ...) end;
375 }, {__index=function(tbl, key)
376 if key == "controller" then
378 elseif key == "REQUEST_URI" then
379 return build_url(unpack(ctx.requestpath))
380 elseif key == "FULL_REQUEST_URI" then
381 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
382 local query = http.getenv("QUERY_STRING")
383 if query and #query > 0 then
387 return table.concat(url, "")
388 elseif key == "token" then
391 return rawget(tbl, key) or _G[key]
396 track.dependent = (track.dependent ~= false)
397 assert(not track.dependent or not track.auto,
398 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
399 "has no parent node so the access to this location has been denied.\n" ..
400 "This is a software bug, please report this message at " ..
401 "https://github.com/openwrt/luci/issues"
404 if track.sysauth and not ctx.authsession then
405 local authen = track.sysauth_authenticator
406 local _, sid, sdat, default_user, allowed_users
408 if type(authen) == "string" and authen ~= "htmlauth" then
409 error500("Unsupported authenticator %q configured" % authen)
413 if type(track.sysauth) == "table" then
414 default_user, allowed_users = nil, track.sysauth
416 default_user, allowed_users = track.sysauth, { track.sysauth }
419 if type(authen) == "function" then
420 _, sid = authen(sys.user.checkpasswd, allowed_users)
422 sid = http.getcookie("sysauth")
425 sid, sdat = session_retrieve(sid, allowed_users)
427 if not (sid and sdat) and authen == "htmlauth" then
428 local user = http.getenv("HTTP_AUTH_USER")
429 local pass = http.getenv("HTTP_AUTH_PASS")
431 if user == nil and pass == nil then
432 user = http.formvalue("luci_username")
433 pass = http.formvalue("luci_password")
436 sid, sdat = session_setup(user, pass, allowed_users)
439 local tmpl = require "luci.template"
443 http.status(403, "Forbidden")
444 http.header("X-LuCI-Login-Required", "yes")
445 tmpl.render(track.sysauth_template or "sysauth", {
446 duser = default_user,
453 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
454 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
456 http.redirect(build_url(unpack(ctx.requestpath)))
459 if not sid or not sdat then
460 http.status(403, "Forbidden")
461 http.header("X-LuCI-Login-Required", "yes")
465 ctx.authsession = sid
466 ctx.authtoken = sdat.token
467 ctx.authuser = sdat.username
470 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
471 luci.http.status(200, "OK")
472 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
473 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
477 if c and require_post_security(c.target, args) then
478 if not test_post_security(c) then
483 if track.setgroup then
484 sys.process.setgroup(track.setgroup)
487 if track.setuser then
488 sys.process.setuser(track.setuser)
493 if type(c.target) == "function" then
495 elseif type(c.target) == "table" then
496 target = c.target.target
500 if c and (c.index or type(target) == "function") then
502 ctx.requested = ctx.requested or ctx.dispatched
505 if c and c.index then
506 local tpl = require "luci.template"
508 if util.copcall(tpl.render, "indexer", {}) then
513 if type(target) == "function" then
514 util.copcall(function()
515 local oldenv = getfenv(target)
516 local module = require(c.module)
517 local env = setmetatable({}, {__index=
520 return rawget(tbl, key) or module[key] or oldenv[key]
527 if type(c.target) == "table" then
528 ok, err = util.copcall(target, c.target, unpack(args))
530 ok, err = util.copcall(target, unpack(args))
533 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
534 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
535 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
539 if not root or not root.target then
540 error404("No root node was registered, this usually happens if no module was installed.\n" ..
541 "Install luci-mod-admin-full and retry. " ..
542 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
544 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
545 "If this url belongs to an extension, make sure it is properly installed.\n" ..
546 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
551 function createindex()
552 local controllers = { }
553 local base = "%s/controller/" % util.libpath()
556 for path in (fs.glob("%s*.lua" % base) or function() end) do
557 controllers[#controllers+1] = path
560 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
561 controllers[#controllers+1] = path
565 local cachedate = fs.stat(indexcache, "mtime")
568 for _, obj in ipairs(controllers) do
569 local omtime = fs.stat(obj, "mtime")
570 realdate = (omtime and omtime > realdate) and omtime or realdate
573 if cachedate > realdate and sys.process.info("uid") == 0 then
575 sys.process.info("uid") == fs.stat(indexcache, "uid")
576 and fs.stat(indexcache, "modestr") == "rw-------",
577 "Fatal: Indexcache is not sane!"
580 index = loadfile(indexcache)()
588 for _, path in ipairs(controllers) do
589 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
590 local mod = require(modname)
592 "Invalid controller file found\n" ..
593 "The file '" .. path .. "' contains an invalid module line.\n" ..
594 "Please verify whether the module name is set to '" .. modname ..
595 "' - It must correspond to the file path!")
597 local idx = mod.index
598 assert(type(idx) == "function",
599 "Invalid controller file found\n" ..
600 "The file '" .. path .. "' contains no index() function.\n" ..
601 "Please make sure that the controller contains a valid " ..
602 "index function and verify the spelling!")
608 local f = nixio.open(indexcache, "w", 600)
609 f:writeall(util.get_bytecode(index))
614 -- Build the index before if it does not exist yet.
615 function createtree()
621 local tree = {nodes={}, inreq=true}
623 ctx.treecache = setmetatable({}, {__mode="v"})
626 local scope = setmetatable({}, {__index = luci.dispatcher})
628 for k, v in pairs(index) do
637 function assign(path, clone, title, order)
638 local obj = node(unpack(path))
645 setmetatable(obj, {__index = _create_node(clone)})
650 function entry(path, target, title, order)
651 local c = node(unpack(path))
656 c.module = getfenv(2)._NAME
661 -- enabling the node.
663 return _create_node({...})
667 local c = _create_node({...})
669 c.module = getfenv(2)._NAME
676 local i, path = nil, {}
677 for i = 1, select('#', ...) do
678 local name, arg = nil, tostring(select(i, ...))
679 for name in arg:gmatch("[^/]+") do
684 for i = #path, 1, -1 do
685 local node = context.treecache[table.concat(path, ".", 1, i)]
686 if node and (i == #path or node.leaf) then
687 return node, build_url(unpack(path))
692 function _create_node(path)
697 local name = table.concat(path, ".")
698 local c = context.treecache[name]
701 local last = table.remove(path)
702 local parent = _create_node(path)
704 c = {nodes={}, auto=true, inreq=true}
707 for _, n in ipairs(path) do
708 if context.path[_] ~= n then
714 c.inreq = c.inreq and (context.path[#path + 1] == last)
716 parent.nodes[last] = c
717 context.treecache[name] = c
725 function _find_eligible_node(root, prefix, deep, types, descend)
726 local children = _ordered_children(root)
728 if not root.leaf and deep ~= nil then
729 local sub_path = { unpack(prefix) }
731 if deep == false then
736 for _, child in ipairs(children) do
737 sub_path[#prefix+1] = child.name
739 local res_path = _find_eligible_node(child.node, sub_path,
750 (type(root.target) == "table" and
751 util.contains(types, root.target.type)))
757 function _find_node(recurse, types)
758 local path = { unpack(context.path) }
759 local name = table.concat(path, ".")
760 local node = context.treecache[name]
762 path = _find_eligible_node(node, path, recurse, types)
767 require "luci.template".render("empty_node_placeholder")
771 function _firstchild()
772 return _find_node(false, nil)
775 function firstchild()
776 return { type = "firstchild", target = _firstchild }
779 function _firstnode()
780 return _find_node(true, { "cbi", "form", "template", "arcombine" })
784 return { type = "firstnode", target = _firstnode }
790 for _, r in ipairs({...}) do
798 function rewrite(n, ...)
801 local dispatched = util.clone(context.dispatched)
804 table.remove(dispatched, 1)
807 for i, r in ipairs(req) do
808 table.insert(dispatched, i, r)
811 for _, r in ipairs({...}) do
812 dispatched[#dispatched+1] = r
820 local function _call(self, ...)
821 local func = getfenv()[self.name]
823 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
825 assert(type(func) == "function",
826 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
827 'of type "' .. type(func) .. '".')
829 if #self.argv > 0 then
830 return func(unpack(self.argv), ...)
836 function call(name, ...)
837 return {type = "call", argv = {...}, name = name, target = _call}
840 function post_on(params, name, ...)
851 return post_on(true, ...)
855 local _template = function(self, ...)
856 require "luci.template".render(self.view)
859 function template(name)
860 return {type = "template", view = name, target = _template}
864 local _view = function(self, ...)
865 require "luci.template".render("view", { view = self.view })
869 return {type = "view", view = name, target = _view}
873 local function _cbi(self, ...)
874 local cbi = require "luci.cbi"
875 local tpl = require "luci.template"
876 local http = require "luci.http"
878 local config = self.config or {}
879 local maps = cbi.load(self.model, ...)
884 for i, res in ipairs(maps) do
885 if util.instanceof(res, cbi.SimpleForm) then
886 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
889 io.stderr:write("please change %s to use the form() action instead.\n"
890 % table.concat(context.request, "/"))
894 local cstate = res:parse()
895 if cstate and (not state or cstate < state) then
900 local function _resolve_path(path)
901 return type(path) == "table" and build_url(unpack(path)) or path
904 if config.on_valid_to and state and state > 0 and state < 2 then
905 http.redirect(_resolve_path(config.on_valid_to))
909 if config.on_changed_to and state and state > 1 then
910 http.redirect(_resolve_path(config.on_changed_to))
914 if config.on_success_to and state and state > 0 then
915 http.redirect(_resolve_path(config.on_success_to))
919 if config.state_handler then
920 if not config.state_handler(state, maps) then
925 http.header("X-CBI-State", state or 0)
927 if not config.noheader then
928 tpl.render("cbi/header", {state = state})
933 local applymap = false
934 local pageaction = true
935 local parsechain = { }
937 for i, res in ipairs(maps) do
938 if res.apply_needed and res.parsechain then
940 for _, c in ipairs(res.parsechain) do
941 parsechain[#parsechain+1] = c
947 redirect = redirect or res.redirect
950 if res.pageaction == false then
955 messages = messages or { }
956 messages[#messages+1] = res.message
960 for i, res in ipairs(maps) do
965 pageaction = pageaction,
966 parsechain = parsechain
970 if not config.nofooter then
971 tpl.render("cbi/footer", {
973 pageaction = pageaction,
976 autoapply = config.autoapply,
977 trigger_apply = applymap
982 function cbi(model, config)
985 post = { ["cbi.submit"] = true },
993 local function _arcombine(self, ...)
995 local target = #argv > 0 and self.targets[2] or self.targets[1]
996 setfenv(target.target, self.env)
997 target:target(unpack(argv))
1000 function arcombine(trg1, trg2)
1001 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1005 local function _form(self, ...)
1006 local cbi = require "luci.cbi"
1007 local tpl = require "luci.template"
1008 local http = require "luci.http"
1010 local maps = luci.cbi.load(self.model, ...)
1014 for i, res in ipairs(maps) do
1015 local cstate = res:parse()
1016 if cstate and (not state or cstate < state) then
1021 http.header("X-CBI-State", state or 0)
1022 tpl.render("header")
1023 for i, res in ipairs(maps) do
1026 tpl.render("footer")
1029 function form(model)
1032 post = { ["cbi.submit"] = true },
1038 translate = i18n.translate
1040 -- This function does not actually translate the given argument but
1041 -- is used by build/i18n-scan.pl to find translatable entries.