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 request_val == nil)
148 return (target.post == true)
154 function test_post_security()
155 if http.getenv("REQUEST_METHOD") ~= "POST" then
156 http.status(405, "Method Not Allowed")
157 http.header("Allow", "POST")
161 if http.formvalue("token") ~= context.authtoken then
162 http.status(403, "Forbidden")
163 luci.template.render("csrftoken")
170 local function session_retrieve(sid, allowed_users)
171 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
173 if type(sdat) == "table" and
174 type(sdat.values) == "table" and
175 type(sdat.values.token) == "string" and
176 (not allowed_users or
177 util.contains(allowed_users, sdat.values.username))
179 return sid, sdat.values
185 local function session_setup(user, pass, allowed_users)
186 if util.contains(allowed_users, user) then
187 local login = util.ubus("session", "login", {
190 timeout = tonumber(luci.config.sauth.sessiontime)
193 local rp = context.requestpath
194 and table.concat(context.requestpath, "/") or ""
196 if type(login) == "table" and
197 type(login.ubus_rpc_session) == "string"
199 util.ubus("session", "set", {
200 ubus_rpc_session = login.ubus_rpc_session,
201 values = { token = sys.uniqueid(16) }
204 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
205 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
207 return session_retrieve(login.ubus_rpc_session)
210 io.stderr:write("luci: failed login on /%s for %s from %s\n"
211 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
217 function dispatch(request)
218 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
222 local conf = require "luci.config"
224 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
226 local i18n = require "luci.i18n"
227 local lang = conf.main.lang or "auto"
228 if lang == "auto" then
229 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
230 for aclang in aclang:gmatch("[%w_-]+") do
231 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
232 if country and culture then
233 local cc = "%s_%s" %{ country, culture:lower() }
234 if conf.languages[cc] then
237 elseif conf.languages[country] then
241 elseif conf.languages[aclang] then
247 if lang == "auto" then
250 i18n.setlanguage(lang)
261 ctx.requestargs = ctx.requestargs or args
266 for i, s in ipairs(request) do
275 util.update(track, c)
283 for j=n+1, #request do
284 args[#args+1] = request[j]
285 freq[#freq+1] = request[j]
289 ctx.requestpath = ctx.requestpath or freq
293 i18n.loadc(track.i18n)
296 -- Init template engine
297 if (c and c.index) or not track.notemplate then
298 local tpl = require("luci.template")
299 local media = track.mediaurlbase or luci.config.main.mediaurlbase
300 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
302 for name, theme in pairs(luci.config.themes) do
303 if name:sub(1,1) ~= "." and pcall(tpl.Template,
304 "themes/%s/header" % fs.basename(theme)) then
308 assert(media, "No valid theme found")
311 local function _ifattr(cond, key, val)
313 local env = getfenv(3)
314 local scope = (type(env.self) == "table") and env.self
315 if type(val) == "table" then
316 if not next(val) then
319 val = util.serialize_json(val)
322 return string.format(
323 ' %s="%s"', tostring(key),
324 util.pcdata(tostring( val
325 or (type(env[key]) ~= "function" and env[key])
326 or (scope and type(scope[key]) ~= "function" and scope[key])
334 tpl.context.viewns = setmetatable({
336 include = function(name) tpl.Template(name):render(getfenv(2)) end;
337 translate = i18n.translate;
338 translatef = i18n.translatef;
339 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
340 striptags = util.striptags;
341 pcdata = util.pcdata;
343 theme = fs.basename(media);
344 resource = luci.config.main.resourcebase;
345 ifattr = function(...) return _ifattr(...) end;
346 attr = function(...) return _ifattr(true, ...) end;
348 }, {__index=function(tbl, key)
349 if key == "controller" then
351 elseif key == "REQUEST_URI" then
352 return build_url(unpack(ctx.requestpath))
353 elseif key == "FULL_REQUEST_URI" then
354 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
355 local query = http.getenv("QUERY_STRING")
356 if query and #query > 0 then
360 return table.concat(url, "")
361 elseif key == "token" then
364 return rawget(tbl, key) or _G[key]
369 track.dependent = (track.dependent ~= false)
370 assert(not track.dependent or not track.auto,
371 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
372 "has no parent node so the access to this location has been denied.\n" ..
373 "This is a software bug, please report this message at " ..
374 "https://github.com/openwrt/luci/issues"
377 if track.sysauth and not ctx.authsession then
378 local authen = track.sysauth_authenticator
379 local _, sid, sdat, default_user, allowed_users
381 if type(authen) == "string" and authen ~= "htmlauth" then
382 error500("Unsupported authenticator %q configured" % authen)
386 if type(track.sysauth) == "table" then
387 default_user, allowed_users = nil, track.sysauth
389 default_user, allowed_users = track.sysauth, { track.sysauth }
392 if type(authen) == "function" then
393 _, sid = authen(sys.user.checkpasswd, allowed_users)
395 sid = http.getcookie("sysauth")
398 sid, sdat = session_retrieve(sid, allowed_users)
400 if not (sid and sdat) and authen == "htmlauth" then
401 local user = http.getenv("HTTP_AUTH_USER")
402 local pass = http.getenv("HTTP_AUTH_PASS")
404 if user == nil and pass == nil then
405 user = http.formvalue("luci_username")
406 pass = http.formvalue("luci_password")
409 sid, sdat = session_setup(user, pass, allowed_users)
412 local tmpl = require "luci.template"
416 http.status(403, "Forbidden")
417 tmpl.render(track.sysauth_template or "sysauth", {
418 duser = default_user,
425 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
426 http.redirect(build_url(unpack(ctx.requestpath)))
429 if not sid or not sdat then
430 http.status(403, "Forbidden")
434 ctx.authsession = sid
435 ctx.authtoken = sdat.token
436 ctx.authuser = sdat.username
439 if c and require_post_security(c.target) then
440 if not test_post_security(c) then
445 if track.setgroup then
446 sys.process.setgroup(track.setgroup)
449 if track.setuser then
450 sys.process.setuser(track.setuser)
455 if type(c.target) == "function" then
457 elseif type(c.target) == "table" then
458 target = c.target.target
462 if c and (c.index or type(target) == "function") then
464 ctx.requested = ctx.requested or ctx.dispatched
467 if c and c.index then
468 local tpl = require "luci.template"
470 if util.copcall(tpl.render, "indexer", {}) then
475 if type(target) == "function" then
476 util.copcall(function()
477 local oldenv = getfenv(target)
478 local module = require(c.module)
479 local env = setmetatable({}, {__index=
482 return rawget(tbl, key) or module[key] or oldenv[key]
489 if type(c.target) == "table" then
490 ok, err = util.copcall(target, c.target, unpack(args))
492 ok, err = util.copcall(target, unpack(args))
495 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
496 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
497 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
500 if not root or not root.target then
501 error404("No root node was registered, this usually happens if no module was installed.\n" ..
502 "Install luci-mod-admin-full and retry. " ..
503 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
505 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
506 "If this url belongs to an extension, make sure it is properly installed.\n" ..
507 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
512 function createindex()
513 local controllers = { }
514 local base = "%s/controller/" % util.libpath()
517 for path in (fs.glob("%s*.lua" % base) or function() end) do
518 controllers[#controllers+1] = path
521 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
522 controllers[#controllers+1] = path
526 local cachedate = fs.stat(indexcache, "mtime")
529 for _, obj in ipairs(controllers) do
530 local omtime = fs.stat(obj, "mtime")
531 realdate = (omtime and omtime > realdate) and omtime or realdate
534 if cachedate > realdate and sys.process.info("uid") == 0 then
536 sys.process.info("uid") == fs.stat(indexcache, "uid")
537 and fs.stat(indexcache, "modestr") == "rw-------",
538 "Fatal: Indexcache is not sane!"
541 index = loadfile(indexcache)()
549 for _, path in ipairs(controllers) do
550 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
551 local mod = require(modname)
553 "Invalid controller file found\n" ..
554 "The file '" .. path .. "' contains an invalid module line.\n" ..
555 "Please verify whether the module name is set to '" .. modname ..
556 "' - It must correspond to the file path!")
558 local idx = mod.index
559 assert(type(idx) == "function",
560 "Invalid controller file found\n" ..
561 "The file '" .. path .. "' contains no index() function.\n" ..
562 "Please make sure that the controller contains a valid " ..
563 "index function and verify the spelling!")
569 local f = nixio.open(indexcache, "w", 600)
570 f:writeall(util.get_bytecode(index))
575 -- Build the index before if it does not exist yet.
576 function createtree()
582 local tree = {nodes={}, inreq=true}
585 ctx.treecache = setmetatable({}, {__mode="v"})
589 -- Load default translation
590 require "luci.i18n".loadc("base")
592 local scope = setmetatable({}, {__index = luci.dispatcher})
594 for k, v in pairs(index) do
600 local function modisort(a,b)
601 return modi[a].order < modi[b].order
604 for _, v in util.spairs(modi, modisort) do
605 scope._NAME = v.module
606 setfenv(v.func, scope)
613 function modifier(func, order)
614 context.modifiers[#context.modifiers+1] = {
622 function assign(path, clone, title, order)
623 local obj = node(unpack(path))
630 setmetatable(obj, {__index = _create_node(clone)})
635 function entry(path, target, title, order)
636 local c = node(unpack(path))
641 c.module = getfenv(2)._NAME
646 -- enabling the node.
648 return _create_node({...})
652 local c = _create_node({...})
654 c.module = getfenv(2)._NAME
661 local i, path = nil, {}
662 for i = 1, select('#', ...) do
663 local name, arg = nil, tostring(select(i, ...))
664 for name in arg:gmatch("[^/]+") do
669 for i = #path, 1, -1 do
670 local node = context.treecache[table.concat(path, ".", 1, i)]
671 if node and (i == #path or node.leaf) then
672 return node, build_url(unpack(path))
677 function _create_node(path)
682 local name = table.concat(path, ".")
683 local c = context.treecache[name]
686 local last = table.remove(path)
687 local parent = _create_node(path)
689 c = {nodes={}, auto=true}
690 -- the node is "in request" if the request path matches
691 -- at least up to the length of the node path
692 if parent.inreq and context.path[#path+1] == last then
695 parent.nodes[last] = c
696 context.treecache[name] = c
703 function _firstchild()
704 local path = { unpack(context.path) }
705 local name = table.concat(path, ".")
706 local node = context.treecache[name]
709 if node and node.nodes and next(node.nodes) then
711 for k, v in pairs(node.nodes) do
713 (v.order or 100) < (node.nodes[lowest].order or 100)
720 assert(lowest ~= nil,
721 "The requested node contains no childs, unable to redispatch")
723 path[#path+1] = lowest
727 function firstchild()
728 return { type = "firstchild", target = _firstchild }
734 for _, r in ipairs({...}) do
742 function rewrite(n, ...)
745 local dispatched = util.clone(context.dispatched)
748 table.remove(dispatched, 1)
751 for i, r in ipairs(req) do
752 table.insert(dispatched, i, r)
755 for _, r in ipairs({...}) do
756 dispatched[#dispatched+1] = r
764 local function _call(self, ...)
765 local func = getfenv()[self.name]
767 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
769 assert(type(func) == "function",
770 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
771 'of type "' .. type(func) .. '".')
773 if #self.argv > 0 then
774 return func(unpack(self.argv), ...)
780 function call(name, ...)
781 return {type = "call", argv = {...}, name = name, target = _call}
784 function post_on(params, name, ...)
795 return post_on(true, ...)
799 local _template = function(self, ...)
800 require "luci.template".render(self.view)
803 function template(name)
804 return {type = "template", view = name, target = _template}
808 local function _cbi(self, ...)
809 local cbi = require "luci.cbi"
810 local tpl = require "luci.template"
811 local http = require "luci.http"
813 local config = self.config or {}
814 local maps = cbi.load(self.model, ...)
819 for i, res in ipairs(maps) do
820 if util.instanceof(res, cbi.SimpleForm) then
821 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
824 io.stderr:write("please change %s to use the form() action instead.\n"
825 % table.concat(context.request, "/"))
829 local cstate = res:parse()
830 if cstate and (not state or cstate < state) then
835 local function _resolve_path(path)
836 return type(path) == "table" and build_url(unpack(path)) or path
839 if config.on_valid_to and state and state > 0 and state < 2 then
840 http.redirect(_resolve_path(config.on_valid_to))
844 if config.on_changed_to and state and state > 1 then
845 http.redirect(_resolve_path(config.on_changed_to))
849 if config.on_success_to and state and state > 0 then
850 http.redirect(_resolve_path(config.on_success_to))
854 if config.state_handler then
855 if not config.state_handler(state, maps) then
860 http.header("X-CBI-State", state or 0)
862 if not config.noheader then
863 tpl.render("cbi/header", {state = state})
868 local applymap = false
869 local pageaction = true
870 local parsechain = { }
872 for i, res in ipairs(maps) do
873 if res.apply_needed and res.parsechain then
875 for _, c in ipairs(res.parsechain) do
876 parsechain[#parsechain+1] = c
882 redirect = redirect or res.redirect
885 if res.pageaction == false then
890 messages = messages or { }
891 messages[#messages+1] = res.message
895 for i, res in ipairs(maps) do
901 pageaction = pageaction,
902 parsechain = parsechain
906 if not config.nofooter then
907 tpl.render("cbi/footer", {
909 pageaction = pageaction,
912 autoapply = config.autoapply
917 function cbi(model, config)
920 post = { ["cbi.submit"] = true },
928 local function _arcombine(self, ...)
930 local target = #argv > 0 and self.targets[2] or self.targets[1]
931 setfenv(target.target, self.env)
932 target:target(unpack(argv))
935 function arcombine(trg1, trg2)
936 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
940 local function _form(self, ...)
941 local cbi = require "luci.cbi"
942 local tpl = require "luci.template"
943 local http = require "luci.http"
945 local maps = luci.cbi.load(self.model, ...)
949 for i, res in ipairs(maps) do
950 local cstate = res:parse()
951 if cstate and (not state or cstate < state) then
956 http.header("X-CBI-State", state or 0)
958 for i, res in ipairs(maps) do
967 post = { ["cbi.submit"] = true },
973 translate = i18n.translate
975 -- This function does not actually translate the given argument but
976 -- is used by build/i18n-scan.pl to find translatable entries.