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(table, key)
350 if key == "controller" then
352 elseif key == "REQUEST_URI" then
353 return build_url(unpack(ctx.requestpath))
354 elseif key == "token" then
357 return rawget(table, key) or _G[key]
362 track.dependent = (track.dependent ~= false)
363 assert(not track.dependent or not track.auto,
364 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
365 "has no parent node so the access to this location has been denied.\n" ..
366 "This is a software bug, please report this message at " ..
367 "https://github.com/openwrt/luci/issues"
370 if track.sysauth and not ctx.authsession then
371 local authen = track.sysauth_authenticator
372 local _, sid, sdat, default_user, allowed_users
374 if type(authen) == "string" and authen ~= "htmlauth" then
375 error500("Unsupported authenticator %q configured" % authen)
379 if type(track.sysauth) == "table" then
380 default_user, allowed_users = nil, track.sysauth
382 default_user, allowed_users = track.sysauth, { track.sysauth }
385 if type(authen) == "function" then
386 _, sid = authen(sys.user.checkpasswd, allowed_users)
388 sid = http.getcookie("sysauth")
391 sid, sdat = session_retrieve(sid, allowed_users)
393 if not (sid and sdat) and authen == "htmlauth" then
394 local user = http.getenv("HTTP_AUTH_USER")
395 local pass = http.getenv("HTTP_AUTH_PASS")
397 if user == nil and pass == nil then
398 user = http.formvalue("luci_username")
399 pass = http.formvalue("luci_password")
402 sid, sdat = session_setup(user, pass, allowed_users)
405 local tmpl = require "luci.template"
409 http.status(403, "Forbidden")
410 tmpl.render(track.sysauth_template or "sysauth", {
411 duser = default_user,
418 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
419 http.redirect(build_url(unpack(ctx.requestpath)))
422 if not sid or not sdat then
423 http.status(403, "Forbidden")
427 ctx.authsession = sid
428 ctx.authtoken = sdat.token
429 ctx.authuser = sdat.username
432 if c and require_post_security(c.target) then
433 if not test_post_security(c) then
438 if track.setgroup then
439 sys.process.setgroup(track.setgroup)
442 if track.setuser then
443 sys.process.setuser(track.setuser)
448 if type(c.target) == "function" then
450 elseif type(c.target) == "table" then
451 target = c.target.target
455 if c and (c.index or type(target) == "function") then
457 ctx.requested = ctx.requested or ctx.dispatched
460 if c and c.index then
461 local tpl = require "luci.template"
463 if util.copcall(tpl.render, "indexer", {}) then
468 if type(target) == "function" then
469 util.copcall(function()
470 local oldenv = getfenv(target)
471 local module = require(c.module)
472 local env = setmetatable({}, {__index=
475 return rawget(tbl, key) or module[key] or oldenv[key]
482 if type(c.target) == "table" then
483 ok, err = util.copcall(target, c.target, unpack(args))
485 ok, err = util.copcall(target, unpack(args))
488 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
489 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
490 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
493 if not root or not root.target then
494 error404("No root node was registered, this usually happens if no module was installed.\n" ..
495 "Install luci-mod-admin-full and retry. " ..
496 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
498 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
499 "If this url belongs to an extension, make sure it is properly installed.\n" ..
500 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
505 function createindex()
506 local controllers = { }
507 local base = "%s/controller/" % util.libpath()
510 for path in (fs.glob("%s*.lua" % base) or function() end) do
511 controllers[#controllers+1] = path
514 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
515 controllers[#controllers+1] = path
519 local cachedate = fs.stat(indexcache, "mtime")
522 for _, obj in ipairs(controllers) do
523 local omtime = fs.stat(obj, "mtime")
524 realdate = (omtime and omtime > realdate) and omtime or realdate
527 if cachedate > realdate and sys.process.info("uid") == 0 then
529 sys.process.info("uid") == fs.stat(indexcache, "uid")
530 and fs.stat(indexcache, "modestr") == "rw-------",
531 "Fatal: Indexcache is not sane!"
534 index = loadfile(indexcache)()
542 for _, path in ipairs(controllers) do
543 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
544 local mod = require(modname)
546 "Invalid controller file found\n" ..
547 "The file '" .. path .. "' contains an invalid module line.\n" ..
548 "Please verify whether the module name is set to '" .. modname ..
549 "' - It must correspond to the file path!")
551 local idx = mod.index
552 assert(type(idx) == "function",
553 "Invalid controller file found\n" ..
554 "The file '" .. path .. "' contains no index() function.\n" ..
555 "Please make sure that the controller contains a valid " ..
556 "index function and verify the spelling!")
562 local f = nixio.open(indexcache, "w", 600)
563 f:writeall(util.get_bytecode(index))
568 -- Build the index before if it does not exist yet.
569 function createtree()
575 local tree = {nodes={}, inreq=true}
578 ctx.treecache = setmetatable({}, {__mode="v"})
582 -- Load default translation
583 require "luci.i18n".loadc("base")
585 local scope = setmetatable({}, {__index = luci.dispatcher})
587 for k, v in pairs(index) do
593 local function modisort(a,b)
594 return modi[a].order < modi[b].order
597 for _, v in util.spairs(modi, modisort) do
598 scope._NAME = v.module
599 setfenv(v.func, scope)
606 function modifier(func, order)
607 context.modifiers[#context.modifiers+1] = {
615 function assign(path, clone, title, order)
616 local obj = node(unpack(path))
623 setmetatable(obj, {__index = _create_node(clone)})
628 function entry(path, target, title, order)
629 local c = node(unpack(path))
634 c.module = getfenv(2)._NAME
639 -- enabling the node.
641 return _create_node({...})
645 local c = _create_node({...})
647 c.module = getfenv(2)._NAME
653 function _create_node(path)
658 local name = table.concat(path, ".")
659 local c = context.treecache[name]
662 local last = table.remove(path)
663 local parent = _create_node(path)
665 c = {nodes={}, auto=true}
666 -- the node is "in request" if the request path matches
667 -- at least up to the length of the node path
668 if parent.inreq and context.path[#path+1] == last then
671 parent.nodes[last] = c
672 context.treecache[name] = c
679 function _firstchild()
680 local path = { unpack(context.path) }
681 local name = table.concat(path, ".")
682 local node = context.treecache[name]
685 if node and node.nodes and next(node.nodes) then
687 for k, v in pairs(node.nodes) do
689 (v.order or 100) < (node.nodes[lowest].order or 100)
696 assert(lowest ~= nil,
697 "The requested node contains no childs, unable to redispatch")
699 path[#path+1] = lowest
703 function firstchild()
704 return { type = "firstchild", target = _firstchild }
710 for _, r in ipairs({...}) do
718 function rewrite(n, ...)
721 local dispatched = util.clone(context.dispatched)
724 table.remove(dispatched, 1)
727 for i, r in ipairs(req) do
728 table.insert(dispatched, i, r)
731 for _, r in ipairs({...}) do
732 dispatched[#dispatched+1] = r
740 local function _call(self, ...)
741 local func = getfenv()[self.name]
743 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
745 assert(type(func) == "function",
746 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
747 'of type "' .. type(func) .. '".')
749 if #self.argv > 0 then
750 return func(unpack(self.argv), ...)
756 function call(name, ...)
757 return {type = "call", argv = {...}, name = name, target = _call}
760 function post_on(params, name, ...)
771 return post_on(true, ...)
775 local _template = function(self, ...)
776 require "luci.template".render(self.view)
779 function template(name)
780 return {type = "template", view = name, target = _template}
784 local function _cbi(self, ...)
785 local cbi = require "luci.cbi"
786 local tpl = require "luci.template"
787 local http = require "luci.http"
789 local config = self.config or {}
790 local maps = cbi.load(self.model, ...)
794 for i, res in ipairs(maps) do
796 local cstate = res:parse()
797 if cstate and (not state or cstate < state) then
802 local function _resolve_path(path)
803 return type(path) == "table" and build_url(unpack(path)) or path
806 if config.on_valid_to and state and state > 0 and state < 2 then
807 http.redirect(_resolve_path(config.on_valid_to))
811 if config.on_changed_to and state and state > 1 then
812 http.redirect(_resolve_path(config.on_changed_to))
816 if config.on_success_to and state and state > 0 then
817 http.redirect(_resolve_path(config.on_success_to))
821 if config.state_handler then
822 if not config.state_handler(state, maps) then
827 http.header("X-CBI-State", state or 0)
829 if not config.noheader then
830 tpl.render("cbi/header", {state = state})
835 local applymap = false
836 local pageaction = true
837 local parsechain = { }
839 for i, res in ipairs(maps) do
840 if res.apply_needed and res.parsechain then
842 for _, c in ipairs(res.parsechain) do
843 parsechain[#parsechain+1] = c
849 redirect = redirect or res.redirect
852 if res.pageaction == false then
857 messages = messages or { }
858 messages[#messages+1] = res.message
862 for i, res in ipairs(maps) do
868 pageaction = pageaction,
869 parsechain = parsechain
873 if not config.nofooter then
874 tpl.render("cbi/footer", {
876 pageaction = pageaction,
879 autoapply = config.autoapply
884 function cbi(model, config)
887 post = { ["cbi.submit"] = "1" },
895 local function _arcombine(self, ...)
897 local target = #argv > 0 and self.targets[2] or self.targets[1]
898 setfenv(target.target, self.env)
899 target:target(unpack(argv))
902 function arcombine(trg1, trg2)
903 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
907 local function _form(self, ...)
908 local cbi = require "luci.cbi"
909 local tpl = require "luci.template"
910 local http = require "luci.http"
912 local maps = luci.cbi.load(self.model, ...)
915 for i, res in ipairs(maps) do
916 local cstate = res:parse()
917 if cstate and (not state or cstate < state) then
922 http.header("X-CBI-State", state or 0)
924 for i, res in ipairs(maps) do
933 post = { ["cbi.submit"] = "1" },
939 translate = i18n.translate
941 -- This function does not actually translate the given argument but
942 -- is used by build/i18n-scan.pl to find translatable entries.