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 local function render()
79 local template = require "luci.template"
80 template.render("error404")
83 if not util.copcall(render) then
84 http.prepare_content("text/plain")
91 function error500(message)
93 if not context.template_header_sent then
94 http.status(500, "Internal Server Error")
95 http.prepare_content("text/plain")
98 require("luci.template")
99 if not util.copcall(luci.template.render, "error500", {message=message}) then
100 http.prepare_content("text/plain")
107 function httpdispatch(request, prefix)
108 http.context.request = request
113 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
116 for _, node in ipairs(prefix) do
122 for node in pathinfo:gmatch("[^/%z]+") do
126 local stat, err = util.coxpcall(function()
127 dispatch(context.request)
132 --context._disable_memtrace()
135 local function require_post_security(target)
136 if type(target) == "table" then
137 if type(target.post) == "table" then
138 local param_name, required_val, request_val
140 for param_name, required_val in pairs(target.post) do
141 request_val = http.formvalue(param_name)
143 if (type(required_val) == "string" and
144 request_val ~= required_val) or
145 (required_val == true and request_val == nil)
154 return (target.post == true)
160 function test_post_security()
161 if http.getenv("REQUEST_METHOD") ~= "POST" then
162 http.status(405, "Method Not Allowed")
163 http.header("Allow", "POST")
167 if http.formvalue("token") ~= context.authtoken then
168 http.status(403, "Forbidden")
169 luci.template.render("csrftoken")
176 local function session_retrieve(sid, allowed_users)
177 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
179 if type(sdat) == "table" and
180 type(sdat.values) == "table" and
181 type(sdat.values.token) == "string" and
182 (not allowed_users or
183 util.contains(allowed_users, sdat.values.username))
185 uci:set_session_id(sid)
186 return sid, sdat.values
192 local function session_setup(user, pass, allowed_users)
193 if util.contains(allowed_users, user) then
194 local login = util.ubus("session", "login", {
197 timeout = tonumber(luci.config.sauth.sessiontime)
200 local rp = context.requestpath
201 and table.concat(context.requestpath, "/") or ""
203 if type(login) == "table" and
204 type(login.ubus_rpc_session) == "string"
206 util.ubus("session", "set", {
207 ubus_rpc_session = login.ubus_rpc_session,
208 values = { token = sys.uniqueid(16) }
211 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
214 return session_retrieve(login.ubus_rpc_session)
217 io.stderr:write("luci: failed login on /%s for %s from %s\n"
218 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
224 function dispatch(request)
225 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
229 local conf = require "luci.config"
231 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
233 local i18n = require "luci.i18n"
234 local lang = conf.main.lang or "auto"
235 if lang == "auto" then
236 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
237 for aclang in aclang:gmatch("[%w_-]+") do
238 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
239 if country and culture then
240 local cc = "%s_%s" %{ country, culture:lower() }
241 if conf.languages[cc] then
244 elseif conf.languages[country] then
248 elseif conf.languages[aclang] then
254 if lang == "auto" then
257 i18n.setlanguage(lang)
268 ctx.requestargs = ctx.requestargs or args
273 for i, s in ipairs(request) do
282 util.update(track, c)
290 for j=n+1, #request do
291 args[#args+1] = request[j]
292 freq[#freq+1] = request[j]
296 ctx.requestpath = ctx.requestpath or freq
300 i18n.loadc(track.i18n)
303 -- Init template engine
304 if (c and c.index) or not track.notemplate then
305 local tpl = require("luci.template")
306 local media = track.mediaurlbase or luci.config.main.mediaurlbase
307 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
309 for name, theme in pairs(luci.config.themes) do
310 if name:sub(1,1) ~= "." and pcall(tpl.Template,
311 "themes/%s/header" % fs.basename(theme)) then
315 assert(media, "No valid theme found")
318 local function _ifattr(cond, key, val)
320 local env = getfenv(3)
321 local scope = (type(env.self) == "table") and env.self
322 if type(val) == "table" then
323 if not next(val) then
326 val = util.serialize_json(val)
329 return string.format(
330 ' %s="%s"', tostring(key),
331 util.pcdata(tostring( val
332 or (type(env[key]) ~= "function" and env[key])
333 or (scope and type(scope[key]) ~= "function" and scope[key])
341 tpl.context.viewns = setmetatable({
343 include = function(name) tpl.Template(name):render(getfenv(2)) end;
344 translate = i18n.translate;
345 translatef = i18n.translatef;
346 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
347 striptags = util.striptags;
348 pcdata = util.pcdata;
350 theme = fs.basename(media);
351 resource = luci.config.main.resourcebase;
352 ifattr = function(...) return _ifattr(...) end;
353 attr = function(...) return _ifattr(true, ...) end;
355 }, {__index=function(tbl, key)
356 if key == "controller" then
358 elseif key == "REQUEST_URI" then
359 return build_url(unpack(ctx.requestpath))
360 elseif key == "FULL_REQUEST_URI" then
361 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
362 local query = http.getenv("QUERY_STRING")
363 if query and #query > 0 then
367 return table.concat(url, "")
368 elseif key == "token" then
371 return rawget(tbl, key) or _G[key]
376 track.dependent = (track.dependent ~= false)
377 assert(not track.dependent or not track.auto,
378 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
379 "has no parent node so the access to this location has been denied.\n" ..
380 "This is a software bug, please report this message at " ..
381 "https://github.com/openwrt/luci/issues"
384 if track.sysauth and not ctx.authsession then
385 local authen = track.sysauth_authenticator
386 local _, sid, sdat, default_user, allowed_users
388 if type(authen) == "string" and authen ~= "htmlauth" then
389 error500("Unsupported authenticator %q configured" % authen)
393 if type(track.sysauth) == "table" then
394 default_user, allowed_users = nil, track.sysauth
396 default_user, allowed_users = track.sysauth, { track.sysauth }
399 if type(authen) == "function" then
400 _, sid = authen(sys.user.checkpasswd, allowed_users)
402 sid = http.getcookie("sysauth")
405 sid, sdat = session_retrieve(sid, allowed_users)
407 if not (sid and sdat) and authen == "htmlauth" then
408 local user = http.getenv("HTTP_AUTH_USER")
409 local pass = http.getenv("HTTP_AUTH_PASS")
411 if user == nil and pass == nil then
412 user = http.formvalue("luci_username")
413 pass = http.formvalue("luci_password")
416 sid, sdat = session_setup(user, pass, allowed_users)
419 local tmpl = require "luci.template"
423 http.status(403, "Forbidden")
424 tmpl.render(track.sysauth_template or "sysauth", {
425 duser = default_user,
432 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
433 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
435 http.redirect(build_url(unpack(ctx.requestpath)))
438 if not sid or not sdat then
439 http.status(403, "Forbidden")
443 ctx.authsession = sid
444 ctx.authtoken = sdat.token
445 ctx.authuser = sdat.username
448 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
449 luci.http.status(200, "OK")
450 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
451 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
455 if c and require_post_security(c.target) then
456 if not test_post_security(c) then
461 if track.setgroup then
462 sys.process.setgroup(track.setgroup)
465 if track.setuser then
466 sys.process.setuser(track.setuser)
471 if type(c.target) == "function" then
473 elseif type(c.target) == "table" then
474 target = c.target.target
478 if c and (c.index or type(target) == "function") then
480 ctx.requested = ctx.requested or ctx.dispatched
483 if c and c.index then
484 local tpl = require "luci.template"
486 if util.copcall(tpl.render, "indexer", {}) then
491 if type(target) == "function" then
492 util.copcall(function()
493 local oldenv = getfenv(target)
494 local module = require(c.module)
495 local env = setmetatable({}, {__index=
498 return rawget(tbl, key) or module[key] or oldenv[key]
505 if type(c.target) == "table" then
506 ok, err = util.copcall(target, c.target, unpack(args))
508 ok, err = util.copcall(target, unpack(args))
511 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
512 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
513 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
517 if not root or not root.target then
518 error404("No root node was registered, this usually happens if no module was installed.\n" ..
519 "Install luci-mod-admin-full and retry. " ..
520 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
522 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
523 "If this url belongs to an extension, make sure it is properly installed.\n" ..
524 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
529 function createindex()
530 local controllers = { }
531 local base = "%s/controller/" % util.libpath()
534 for path in (fs.glob("%s*.lua" % base) or function() end) do
535 controllers[#controllers+1] = path
538 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
539 controllers[#controllers+1] = path
543 local cachedate = fs.stat(indexcache, "mtime")
546 for _, obj in ipairs(controllers) do
547 local omtime = fs.stat(obj, "mtime")
548 realdate = (omtime and omtime > realdate) and omtime or realdate
551 if cachedate > realdate and sys.process.info("uid") == 0 then
553 sys.process.info("uid") == fs.stat(indexcache, "uid")
554 and fs.stat(indexcache, "modestr") == "rw-------",
555 "Fatal: Indexcache is not sane!"
558 index = loadfile(indexcache)()
566 for _, path in ipairs(controllers) do
567 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
568 local mod = require(modname)
570 "Invalid controller file found\n" ..
571 "The file '" .. path .. "' contains an invalid module line.\n" ..
572 "Please verify whether the module name is set to '" .. modname ..
573 "' - It must correspond to the file path!")
575 local idx = mod.index
576 assert(type(idx) == "function",
577 "Invalid controller file found\n" ..
578 "The file '" .. path .. "' contains no index() function.\n" ..
579 "Please make sure that the controller contains a valid " ..
580 "index function and verify the spelling!")
586 local f = nixio.open(indexcache, "w", 600)
587 f:writeall(util.get_bytecode(index))
592 -- Build the index before if it does not exist yet.
593 function createtree()
599 local tree = {nodes={}, inreq=true}
602 ctx.treecache = setmetatable({}, {__mode="v"})
606 -- Load default translation
607 require "luci.i18n".loadc("base")
609 local scope = setmetatable({}, {__index = luci.dispatcher})
611 for k, v in pairs(index) do
617 local function modisort(a,b)
618 return modi[a].order < modi[b].order
621 for _, v in util.spairs(modi, modisort) do
622 scope._NAME = v.module
623 setfenv(v.func, scope)
630 function modifier(func, order)
631 context.modifiers[#context.modifiers+1] = {
639 function assign(path, clone, title, order)
640 local obj = node(unpack(path))
647 setmetatable(obj, {__index = _create_node(clone)})
652 function entry(path, target, title, order)
653 local c = node(unpack(path))
658 c.module = getfenv(2)._NAME
663 -- enabling the node.
665 return _create_node({...})
669 local c = _create_node({...})
671 c.module = getfenv(2)._NAME
678 local i, path = nil, {}
679 for i = 1, select('#', ...) do
680 local name, arg = nil, tostring(select(i, ...))
681 for name in arg:gmatch("[^/]+") do
686 for i = #path, 1, -1 do
687 local node = context.treecache[table.concat(path, ".", 1, i)]
688 if node and (i == #path or node.leaf) then
689 return node, build_url(unpack(path))
694 function _create_node(path)
699 local name = table.concat(path, ".")
700 local c = context.treecache[name]
703 local last = table.remove(path)
704 local parent = _create_node(path)
706 c = {nodes={}, auto=true, inreq=true}
709 for _, n in ipairs(path) do
710 if context.path[_] ~= n then
716 c.inreq = c.inreq and (context.path[#path + 1] == last)
718 parent.nodes[last] = c
719 context.treecache[name] = c
727 function _find_eligible_node(root, prefix, deep, types, descend)
728 local _, cur_name, cur_node
731 for cur_name, cur_node in pairs(root.nodes) do
732 childs[#childs+1] = {
735 order = cur_node.order or 100
739 table.sort(childs, function(a, b)
740 if a.order == b.order then
741 return a.name < b.name
743 return a.order < b.order
747 if not root.leaf and deep ~= nil then
748 local sub_path = { unpack(prefix) }
750 if deep == false then
754 for _, cur_node in ipairs(childs) do
755 sub_path[#prefix+1] = cur_node.name
757 local res_path = _find_eligible_node(cur_node.node, sub_path,
768 (type(root.target) == "table" and
769 util.contains(types, root.target.type)))
775 function _find_node(recurse, types)
776 local path = { unpack(context.path) }
777 local name = table.concat(path, ".")
778 local node = context.treecache[name]
780 path = _find_eligible_node(node, path, recurse, types)
785 require "luci.template".render("empty_node_placeholder")
789 function _firstchild()
790 return _find_node(false, nil)
793 function firstchild()
794 return { type = "firstchild", target = _firstchild }
797 function _firstnode()
798 return _find_node(true, { "cbi", "form", "template", "arcombine" })
802 return { type = "firstnode", target = _firstnode }
808 for _, r in ipairs({...}) do
816 function rewrite(n, ...)
819 local dispatched = util.clone(context.dispatched)
822 table.remove(dispatched, 1)
825 for i, r in ipairs(req) do
826 table.insert(dispatched, i, r)
829 for _, r in ipairs({...}) do
830 dispatched[#dispatched+1] = r
838 local function _call(self, ...)
839 local func = getfenv()[self.name]
841 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
843 assert(type(func) == "function",
844 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
845 'of type "' .. type(func) .. '".')
847 if #self.argv > 0 then
848 return func(unpack(self.argv), ...)
854 function call(name, ...)
855 return {type = "call", argv = {...}, name = name, target = _call}
858 function post_on(params, name, ...)
869 return post_on(true, ...)
873 local _template = function(self, ...)
874 require "luci.template".render(self.view)
877 function template(name)
878 return {type = "template", view = name, target = _template}
882 local function _cbi(self, ...)
883 local cbi = require "luci.cbi"
884 local tpl = require "luci.template"
885 local http = require "luci.http"
887 local config = self.config or {}
888 local maps = cbi.load(self.model, ...)
893 for i, res in ipairs(maps) do
894 if util.instanceof(res, cbi.SimpleForm) then
895 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
898 io.stderr:write("please change %s to use the form() action instead.\n"
899 % table.concat(context.request, "/"))
903 local cstate = res:parse()
904 if cstate and (not state or cstate < state) then
909 local function _resolve_path(path)
910 return type(path) == "table" and build_url(unpack(path)) or path
913 if config.on_valid_to and state and state > 0 and state < 2 then
914 http.redirect(_resolve_path(config.on_valid_to))
918 if config.on_changed_to and state and state > 1 then
919 http.redirect(_resolve_path(config.on_changed_to))
923 if config.on_success_to and state and state > 0 then
924 http.redirect(_resolve_path(config.on_success_to))
928 if config.state_handler then
929 if not config.state_handler(state, maps) then
934 http.header("X-CBI-State", state or 0)
936 if not config.noheader then
937 tpl.render("cbi/header", {state = state})
942 local applymap = false
943 local pageaction = true
944 local parsechain = { }
946 for i, res in ipairs(maps) do
947 if res.apply_needed and res.parsechain then
949 for _, c in ipairs(res.parsechain) do
950 parsechain[#parsechain+1] = c
956 redirect = redirect or res.redirect
959 if res.pageaction == false then
964 messages = messages or { }
965 messages[#messages+1] = res.message
969 for i, res in ipairs(maps) do
974 pageaction = pageaction,
975 parsechain = parsechain
979 if not config.nofooter then
980 tpl.render("cbi/footer", {
982 pageaction = pageaction,
985 autoapply = config.autoapply,
986 trigger_apply = applymap
991 function cbi(model, config)
994 post = { ["cbi.submit"] = true },
1002 local function _arcombine(self, ...)
1004 local target = #argv > 0 and self.targets[2] or self.targets[1]
1005 setfenv(target.target, self.env)
1006 target:target(unpack(argv))
1009 function arcombine(trg1, trg2)
1010 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1014 local function _form(self, ...)
1015 local cbi = require "luci.cbi"
1016 local tpl = require "luci.template"
1017 local http = require "luci.http"
1019 local maps = luci.cbi.load(self.model, ...)
1023 for i, res in ipairs(maps) do
1024 local cstate = res:parse()
1025 if cstate and (not state or cstate < state) then
1030 http.header("X-CBI-State", state or 0)
1031 tpl.render("header")
1032 for i, res in ipairs(maps) do
1035 tpl.render("footer")
1038 function form(model)
1041 post = { ["cbi.submit"] = true },
1047 translate = i18n.translate
1049 -- This function does not actually translate the given argument but
1050 -- is used by build/i18n-scan.pl to find translatable entries.