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 return sid, sdat.values
191 local function session_setup(user, pass, allowed_users)
192 if util.contains(allowed_users, user) then
193 local login = util.ubus("session", "login", {
196 timeout = tonumber(luci.config.sauth.sessiontime)
199 local rp = context.requestpath
200 and table.concat(context.requestpath, "/") or ""
202 if type(login) == "table" and
203 type(login.ubus_rpc_session) == "string"
205 util.ubus("session", "set", {
206 ubus_rpc_session = login.ubus_rpc_session,
207 values = { token = sys.uniqueid(16) }
210 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
211 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
213 return session_retrieve(login.ubus_rpc_session)
216 io.stderr:write("luci: failed login on /%s for %s from %s\n"
217 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
223 function dispatch(request)
224 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
228 local conf = require "luci.config"
230 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
232 local i18n = require "luci.i18n"
233 local lang = conf.main.lang or "auto"
234 if lang == "auto" then
235 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
236 for aclang in aclang:gmatch("[%w_-]+") do
237 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
238 if country and culture then
239 local cc = "%s_%s" %{ country, culture:lower() }
240 if conf.languages[cc] then
243 elseif conf.languages[country] then
247 elseif conf.languages[aclang] then
253 if lang == "auto" then
256 i18n.setlanguage(lang)
267 ctx.requestargs = ctx.requestargs or args
272 for i, s in ipairs(request) do
281 util.update(track, c)
289 for j=n+1, #request do
290 args[#args+1] = request[j]
291 freq[#freq+1] = request[j]
295 ctx.requestpath = ctx.requestpath or freq
299 i18n.loadc(track.i18n)
302 -- Init template engine
303 if (c and c.index) or not track.notemplate then
304 local tpl = require("luci.template")
305 local media = track.mediaurlbase or luci.config.main.mediaurlbase
306 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
308 for name, theme in pairs(luci.config.themes) do
309 if name:sub(1,1) ~= "." and pcall(tpl.Template,
310 "themes/%s/header" % fs.basename(theme)) then
314 assert(media, "No valid theme found")
317 local function _ifattr(cond, key, val)
319 local env = getfenv(3)
320 local scope = (type(env.self) == "table") and env.self
321 if type(val) == "table" then
322 if not next(val) then
325 val = util.serialize_json(val)
328 return string.format(
329 ' %s="%s"', tostring(key),
330 util.pcdata(tostring( val
331 or (type(env[key]) ~= "function" and env[key])
332 or (scope and type(scope[key]) ~= "function" and scope[key])
340 tpl.context.viewns = setmetatable({
342 include = function(name) tpl.Template(name):render(getfenv(2)) end;
343 translate = i18n.translate;
344 translatef = i18n.translatef;
345 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
346 striptags = util.striptags;
347 pcdata = util.pcdata;
349 theme = fs.basename(media);
350 resource = luci.config.main.resourcebase;
351 ifattr = function(...) return _ifattr(...) end;
352 attr = function(...) return _ifattr(true, ...) end;
354 }, {__index=function(tbl, key)
355 if key == "controller" then
357 elseif key == "REQUEST_URI" then
358 return build_url(unpack(ctx.requestpath))
359 elseif key == "FULL_REQUEST_URI" then
360 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
361 local query = http.getenv("QUERY_STRING")
362 if query and #query > 0 then
366 return table.concat(url, "")
367 elseif key == "token" then
370 return rawget(tbl, key) or _G[key]
375 track.dependent = (track.dependent ~= false)
376 assert(not track.dependent or not track.auto,
377 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
378 "has no parent node so the access to this location has been denied.\n" ..
379 "This is a software bug, please report this message at " ..
380 "https://github.com/openwrt/luci/issues"
383 if track.sysauth and not ctx.authsession then
384 local authen = track.sysauth_authenticator
385 local _, sid, sdat, default_user, allowed_users
387 if type(authen) == "string" and authen ~= "htmlauth" then
388 error500("Unsupported authenticator %q configured" % authen)
392 if type(track.sysauth) == "table" then
393 default_user, allowed_users = nil, track.sysauth
395 default_user, allowed_users = track.sysauth, { track.sysauth }
398 if type(authen) == "function" then
399 _, sid = authen(sys.user.checkpasswd, allowed_users)
401 sid = http.getcookie("sysauth")
404 sid, sdat = session_retrieve(sid, allowed_users)
406 if not (sid and sdat) and authen == "htmlauth" then
407 local user = http.getenv("HTTP_AUTH_USER")
408 local pass = http.getenv("HTTP_AUTH_PASS")
410 if user == nil and pass == nil then
411 user = http.formvalue("luci_username")
412 pass = http.formvalue("luci_password")
415 sid, sdat = session_setup(user, pass, allowed_users)
418 local tmpl = require "luci.template"
422 http.status(403, "Forbidden")
423 tmpl.render(track.sysauth_template or "sysauth", {
424 duser = default_user,
431 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
432 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
434 http.redirect(build_url(unpack(ctx.requestpath)))
437 if not sid or not sdat then
438 http.status(403, "Forbidden")
442 ctx.authsession = sid
443 ctx.authtoken = sdat.token
444 ctx.authuser = sdat.username
447 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
448 luci.http.status(200, "OK")
449 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
450 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
454 if c and require_post_security(c.target) then
455 if not test_post_security(c) then
460 if track.setgroup then
461 sys.process.setgroup(track.setgroup)
464 if track.setuser then
465 sys.process.setuser(track.setuser)
470 if type(c.target) == "function" then
472 elseif type(c.target) == "table" then
473 target = c.target.target
477 if c and (c.index or type(target) == "function") then
479 ctx.requested = ctx.requested or ctx.dispatched
482 if c and c.index then
483 local tpl = require "luci.template"
485 if util.copcall(tpl.render, "indexer", {}) then
490 if type(target) == "function" then
491 util.copcall(function()
492 local oldenv = getfenv(target)
493 local module = require(c.module)
494 local env = setmetatable({}, {__index=
497 return rawget(tbl, key) or module[key] or oldenv[key]
504 if type(c.target) == "table" then
505 ok, err = util.copcall(target, c.target, unpack(args))
507 ok, err = util.copcall(target, unpack(args))
510 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
511 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
512 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
515 if not root or not root.target then
516 error404("No root node was registered, this usually happens if no module was installed.\n" ..
517 "Install luci-mod-admin-full and retry. " ..
518 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
520 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
521 "If this url belongs to an extension, make sure it is properly installed.\n" ..
522 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
527 function createindex()
528 local controllers = { }
529 local base = "%s/controller/" % util.libpath()
532 for path in (fs.glob("%s*.lua" % base) or function() end) do
533 controllers[#controllers+1] = path
536 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
537 controllers[#controllers+1] = path
541 local cachedate = fs.stat(indexcache, "mtime")
544 for _, obj in ipairs(controllers) do
545 local omtime = fs.stat(obj, "mtime")
546 realdate = (omtime and omtime > realdate) and omtime or realdate
549 if cachedate > realdate and sys.process.info("uid") == 0 then
551 sys.process.info("uid") == fs.stat(indexcache, "uid")
552 and fs.stat(indexcache, "modestr") == "rw-------",
553 "Fatal: Indexcache is not sane!"
556 index = loadfile(indexcache)()
564 for _, path in ipairs(controllers) do
565 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
566 local mod = require(modname)
568 "Invalid controller file found\n" ..
569 "The file '" .. path .. "' contains an invalid module line.\n" ..
570 "Please verify whether the module name is set to '" .. modname ..
571 "' - It must correspond to the file path!")
573 local idx = mod.index
574 assert(type(idx) == "function",
575 "Invalid controller file found\n" ..
576 "The file '" .. path .. "' contains no index() function.\n" ..
577 "Please make sure that the controller contains a valid " ..
578 "index function and verify the spelling!")
584 local f = nixio.open(indexcache, "w", 600)
585 f:writeall(util.get_bytecode(index))
590 -- Build the index before if it does not exist yet.
591 function createtree()
597 local tree = {nodes={}, inreq=true}
600 ctx.treecache = setmetatable({}, {__mode="v"})
604 -- Load default translation
605 require "luci.i18n".loadc("base")
607 local scope = setmetatable({}, {__index = luci.dispatcher})
609 for k, v in pairs(index) do
615 local function modisort(a,b)
616 return modi[a].order < modi[b].order
619 for _, v in util.spairs(modi, modisort) do
620 scope._NAME = v.module
621 setfenv(v.func, scope)
628 function modifier(func, order)
629 context.modifiers[#context.modifiers+1] = {
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}
705 -- the node is "in request" if the request path matches
706 -- at least up to the length of the node path
707 if parent.inreq and context.path[#path+1] == last then
710 parent.nodes[last] = c
711 context.treecache[name] = c
718 function _firstchild()
719 local path = { unpack(context.path) }
720 local name = table.concat(path, ".")
721 local node = context.treecache[name]
724 if node and node.nodes and next(node.nodes) then
726 for k, v in pairs(node.nodes) do
728 (v.order or 100) < (node.nodes[lowest].order or 100)
735 assert(lowest ~= nil,
736 "The requested node contains no childs, unable to redispatch")
738 path[#path+1] = lowest
742 function firstchild()
743 return { type = "firstchild", target = _firstchild }
749 for _, r in ipairs({...}) do
757 function rewrite(n, ...)
760 local dispatched = util.clone(context.dispatched)
763 table.remove(dispatched, 1)
766 for i, r in ipairs(req) do
767 table.insert(dispatched, i, r)
770 for _, r in ipairs({...}) do
771 dispatched[#dispatched+1] = r
779 local function _call(self, ...)
780 local func = getfenv()[self.name]
782 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
784 assert(type(func) == "function",
785 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
786 'of type "' .. type(func) .. '".')
788 if #self.argv > 0 then
789 return func(unpack(self.argv), ...)
795 function call(name, ...)
796 return {type = "call", argv = {...}, name = name, target = _call}
799 function post_on(params, name, ...)
810 return post_on(true, ...)
814 local _template = function(self, ...)
815 require "luci.template".render(self.view)
818 function template(name)
819 return {type = "template", view = name, target = _template}
823 local function _cbi(self, ...)
824 local cbi = require "luci.cbi"
825 local tpl = require "luci.template"
826 local http = require "luci.http"
828 local config = self.config or {}
829 local maps = cbi.load(self.model, ...)
834 for i, res in ipairs(maps) do
835 if util.instanceof(res, cbi.SimpleForm) then
836 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
839 io.stderr:write("please change %s to use the form() action instead.\n"
840 % table.concat(context.request, "/"))
844 local cstate = res:parse()
845 if cstate and (not state or cstate < state) then
850 local function _resolve_path(path)
851 return type(path) == "table" and build_url(unpack(path)) or path
854 if config.on_valid_to and state and state > 0 and state < 2 then
855 http.redirect(_resolve_path(config.on_valid_to))
859 if config.on_changed_to and state and state > 1 then
860 http.redirect(_resolve_path(config.on_changed_to))
864 if config.on_success_to and state and state > 0 then
865 http.redirect(_resolve_path(config.on_success_to))
869 if config.state_handler then
870 if not config.state_handler(state, maps) then
875 http.header("X-CBI-State", state or 0)
877 if not config.noheader then
878 tpl.render("cbi/header", {state = state})
883 local applymap = false
884 local pageaction = true
885 local parsechain = { }
887 for i, res in ipairs(maps) do
888 if res.apply_needed and res.parsechain then
890 for _, c in ipairs(res.parsechain) do
891 parsechain[#parsechain+1] = c
897 redirect = redirect or res.redirect
900 if res.pageaction == false then
905 messages = messages or { }
906 messages[#messages+1] = res.message
910 for i, res in ipairs(maps) do
916 pageaction = pageaction,
917 parsechain = parsechain
921 if not config.nofooter then
922 tpl.render("cbi/footer", {
924 pageaction = pageaction,
927 autoapply = config.autoapply
932 function cbi(model, config)
935 post = { ["cbi.submit"] = true },
943 local function _arcombine(self, ...)
945 local target = #argv > 0 and self.targets[2] or self.targets[1]
946 setfenv(target.target, self.env)
947 target:target(unpack(argv))
950 function arcombine(trg1, trg2)
951 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
955 local function _form(self, ...)
956 local cbi = require "luci.cbi"
957 local tpl = require "luci.template"
958 local http = require "luci.http"
960 local maps = luci.cbi.load(self.model, ...)
964 for i, res in ipairs(maps) do
965 local cstate = res:parse()
966 if cstate and (not state or cstate < state) then
971 http.header("X-CBI-State", state or 0)
973 for i, res in ipairs(maps) do
982 post = { ["cbi.submit"] = true },
988 translate = i18n.translate
990 -- This function does not actually translate the given argument but
991 -- is used by build/i18n-scan.pl to find translatable entries.