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"
26 function build_url(...)
28 local url = { http.getenv("SCRIPT_NAME") or "" }
31 for _, p in ipairs(path) do
32 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
42 return table.concat(url, "")
45 function node_visible(node)
48 (not node.title or #node.title == 0) or
49 (not node.target or node.hidden == true) or
50 (type(node.target) == "table" and node.target.type == "firstchild" and
51 (type(node.nodes) ~= "table" or not next(node.nodes)))
57 function node_childs(node)
61 for k, v in util.spairs(node.nodes,
63 return (node.nodes[a].order or 100)
64 < (node.nodes[b].order or 100)
67 if node_visible(v) then
76 function error404(message)
77 http.status(404, "Not Found")
78 message = message or "Not Found"
80 require("luci.template")
81 if not util.copcall(luci.template.render, "error404") then
82 http.prepare_content("text/plain")
88 function error500(message)
90 if not context.template_header_sent then
91 http.status(500, "Internal Server Error")
92 http.prepare_content("text/plain")
95 require("luci.template")
96 if not util.copcall(luci.template.render, "error500", {message=message}) then
97 http.prepare_content("text/plain")
104 function authenticator.htmlauth(validator, accs, default)
105 local user = http.formvalue("luci_username")
106 local pass = http.formvalue("luci_password")
108 if user and validator(user, pass) then
113 require("luci.template")
115 http.status(403, "Forbidden")
116 luci.template.render("sysauth", {duser=default, fuser=user})
122 function httpdispatch(request, prefix)
123 http.context.request = request
128 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
131 for _, node in ipairs(prefix) do
136 for node in pathinfo:gmatch("[^/]+") do
140 local stat, err = util.coxpcall(function()
141 dispatch(context.request)
146 --context._disable_memtrace()
149 local function require_post_security(target)
150 if type(target) == "table" then
151 if type(target.post) == "table" then
152 local param_name, required_val, request_val
154 for param_name, required_val in pairs(target.post) do
155 request_val = http.formvalue(param_name)
157 if (type(required_val) == "string" and
158 request_val ~= required_val) or
159 (required_val == true and
160 (request_val == nil or request_val == ""))
169 return (target.post == true)
175 function test_post_security()
176 if http.getenv("REQUEST_METHOD") ~= "POST" then
177 http.status(405, "Method Not Allowed")
178 http.header("Allow", "POST")
182 if http.formvalue("token") ~= context.authtoken then
183 http.status(403, "Forbidden")
184 luci.template.render("csrftoken")
191 function dispatch(request)
192 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
196 local conf = require "luci.config"
198 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
200 local lang = conf.main.lang or "auto"
201 if lang == "auto" then
202 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
203 for lpat in aclang:gmatch("[%w-]+") do
204 lpat = lpat and lpat:gsub("-", "_")
205 if conf.languages[lpat] then
211 require "luci.i18n".setlanguage(lang)
222 ctx.requestargs = ctx.requestargs or args
227 for i, s in ipairs(request) do
236 util.update(track, c)
244 for j=n+1, #request do
245 args[#args+1] = request[j]
246 freq[#freq+1] = request[j]
250 ctx.requestpath = ctx.requestpath or freq
254 i18n.loadc(track.i18n)
257 -- Init template engine
258 if (c and c.index) or not track.notemplate then
259 local tpl = require("luci.template")
260 local media = track.mediaurlbase or luci.config.main.mediaurlbase
261 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
263 for name, theme in pairs(luci.config.themes) do
264 if name:sub(1,1) ~= "." and pcall(tpl.Template,
265 "themes/%s/header" % fs.basename(theme)) then
269 assert(media, "No valid theme found")
272 local function _ifattr(cond, key, val)
274 local env = getfenv(3)
275 local scope = (type(env.self) == "table") and env.self
276 if type(val) == "table" then
277 if not next(val) then
280 val = util.serialize_json(val)
283 return string.format(
284 ' %s="%s"', tostring(key),
285 util.pcdata(tostring( val
286 or (type(env[key]) ~= "function" and env[key])
287 or (scope and type(scope[key]) ~= "function" and scope[key])
295 tpl.context.viewns = setmetatable({
297 include = function(name) tpl.Template(name):render(getfenv(2)) end;
298 translate = i18n.translate;
299 translatef = i18n.translatef;
300 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
301 striptags = util.striptags;
302 pcdata = util.pcdata;
304 theme = fs.basename(media);
305 resource = luci.config.main.resourcebase;
306 ifattr = function(...) return _ifattr(...) end;
307 attr = function(...) return _ifattr(true, ...) end;
309 }, {__index=function(table, key)
310 if key == "controller" then
312 elseif key == "REQUEST_URI" then
313 return build_url(unpack(ctx.requestpath))
314 elseif key == "token" then
317 return rawget(table, key) or _G[key]
322 track.dependent = (track.dependent ~= false)
323 assert(not track.dependent or not track.auto,
324 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
325 "has no parent node so the access to this location has been denied.\n" ..
326 "This is a software bug, please report this message at " ..
327 "https://github.com/openwrt/luci/issues"
330 if track.sysauth then
331 local authen = type(track.sysauth_authenticator) == "function"
332 and track.sysauth_authenticator
333 or authenticator[track.sysauth_authenticator]
335 local def = (type(track.sysauth) == "string") and track.sysauth
336 local accs = def and {track.sysauth} or track.sysauth
337 local sess = ctx.authsession
339 sess = http.getcookie("sysauth")
340 sess = sess and sess:match("^[a-f0-9]*$")
343 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
350 local eu = http.getenv("HTTP_AUTH_USER")
351 local ep = http.getenv("HTTP_AUTH_PASS")
352 if eu and ep and sys.user.checkpasswd(eu, ep) then
353 authen = function() return eu end
357 if not util.contains(accs, user) then
359 local user, sess = authen(sys.user.checkpasswd, accs, def)
361 if not user or not util.contains(accs, user) then
365 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
367 token = sys.uniqueid(16)
368 util.ubus("session", "set", {
369 ubus_rpc_session = sdat.ubus_rpc_session,
373 section = sys.uniqueid(16)
376 sess = sdat.ubus_rpc_session
380 if sess and token then
381 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
383 ctx.authsession = sess
384 ctx.authtoken = token
387 http.redirect(build_url(unpack(ctx.requestpath)))
391 http.status(403, "Forbidden")
395 ctx.authsession = sess
396 ctx.authtoken = token
401 if c and require_post_security(c.target) then
402 if not test_post_security(c) then
407 if track.setgroup then
408 sys.process.setgroup(track.setgroup)
411 if track.setuser then
412 sys.process.setuser(track.setuser)
417 if type(c.target) == "function" then
419 elseif type(c.target) == "table" then
420 target = c.target.target
424 if c and (c.index or type(target) == "function") then
426 ctx.requested = ctx.requested or ctx.dispatched
429 if c and c.index then
430 local tpl = require "luci.template"
432 if util.copcall(tpl.render, "indexer", {}) then
437 if type(target) == "function" then
438 util.copcall(function()
439 local oldenv = getfenv(target)
440 local module = require(c.module)
441 local env = setmetatable({}, {__index=
444 return rawget(tbl, key) or module[key] or oldenv[key]
451 if type(c.target) == "table" then
452 ok, err = util.copcall(target, c.target, unpack(args))
454 ok, err = util.copcall(target, unpack(args))
457 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
458 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
459 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
462 if not root or not root.target then
463 error404("No root node was registered, this usually happens if no module was installed.\n" ..
464 "Install luci-mod-admin-full and retry. " ..
465 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
467 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
468 "If this url belongs to an extension, make sure it is properly installed.\n" ..
469 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
474 function createindex()
475 local controllers = { }
476 local base = "%s/controller/" % util.libpath()
479 for path in (fs.glob("%s*.lua" % base) or function() end) do
480 controllers[#controllers+1] = path
483 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
484 controllers[#controllers+1] = path
488 local cachedate = fs.stat(indexcache, "mtime")
491 for _, obj in ipairs(controllers) do
492 local omtime = fs.stat(obj, "mtime")
493 realdate = (omtime and omtime > realdate) and omtime or realdate
496 if cachedate > realdate and sys.process.info("uid") == 0 then
498 sys.process.info("uid") == fs.stat(indexcache, "uid")
499 and fs.stat(indexcache, "modestr") == "rw-------",
500 "Fatal: Indexcache is not sane!"
503 index = loadfile(indexcache)()
511 for _, path in ipairs(controllers) do
512 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
513 local mod = require(modname)
515 "Invalid controller file found\n" ..
516 "The file '" .. path .. "' contains an invalid module line.\n" ..
517 "Please verify whether the module name is set to '" .. modname ..
518 "' - It must correspond to the file path!")
520 local idx = mod.index
521 assert(type(idx) == "function",
522 "Invalid controller file found\n" ..
523 "The file '" .. path .. "' contains no index() function.\n" ..
524 "Please make sure that the controller contains a valid " ..
525 "index function and verify the spelling!")
531 local f = nixio.open(indexcache, "w", 600)
532 f:writeall(util.get_bytecode(index))
537 -- Build the index before if it does not exist yet.
538 function createtree()
544 local tree = {nodes={}, inreq=true}
547 ctx.treecache = setmetatable({}, {__mode="v"})
551 -- Load default translation
552 require "luci.i18n".loadc("base")
554 local scope = setmetatable({}, {__index = luci.dispatcher})
556 for k, v in pairs(index) do
562 local function modisort(a,b)
563 return modi[a].order < modi[b].order
566 for _, v in util.spairs(modi, modisort) do
567 scope._NAME = v.module
568 setfenv(v.func, scope)
575 function modifier(func, order)
576 context.modifiers[#context.modifiers+1] = {
584 function assign(path, clone, title, order)
585 local obj = node(unpack(path))
592 setmetatable(obj, {__index = _create_node(clone)})
597 function entry(path, target, title, order)
598 local c = node(unpack(path))
603 c.module = getfenv(2)._NAME
608 -- enabling the node.
610 return _create_node({...})
614 local c = _create_node({...})
616 c.module = getfenv(2)._NAME
622 function _create_node(path)
627 local name = table.concat(path, ".")
628 local c = context.treecache[name]
631 local last = table.remove(path)
632 local parent = _create_node(path)
634 c = {nodes={}, auto=true}
635 -- the node is "in request" if the request path matches
636 -- at least up to the length of the node path
637 if parent.inreq and context.path[#path+1] == last then
640 parent.nodes[last] = c
641 context.treecache[name] = c
648 function _firstchild()
649 local path = { unpack(context.path) }
650 local name = table.concat(path, ".")
651 local node = context.treecache[name]
654 if node and node.nodes and next(node.nodes) then
656 for k, v in pairs(node.nodes) do
658 (v.order or 100) < (node.nodes[lowest].order or 100)
665 assert(lowest ~= nil,
666 "The requested node contains no childs, unable to redispatch")
668 path[#path+1] = lowest
672 function firstchild()
673 return { type = "firstchild", target = _firstchild }
679 for _, r in ipairs({...}) do
687 function rewrite(n, ...)
690 local dispatched = util.clone(context.dispatched)
693 table.remove(dispatched, 1)
696 for i, r in ipairs(req) do
697 table.insert(dispatched, i, r)
700 for _, r in ipairs({...}) do
701 dispatched[#dispatched+1] = r
709 local function _call(self, ...)
710 local func = getfenv()[self.name]
712 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
714 assert(type(func) == "function",
715 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
716 'of type "' .. type(func) .. '".')
718 if #self.argv > 0 then
719 return func(unpack(self.argv), ...)
725 function call(name, ...)
726 return {type = "call", argv = {...}, name = name, target = _call}
729 function post_on(params, name, ...)
740 return post_on(true, ...)
744 local _template = function(self, ...)
745 require "luci.template".render(self.view)
748 function template(name)
749 return {type = "template", view = name, target = _template}
753 local function _cbi(self, ...)
754 local cbi = require "luci.cbi"
755 local tpl = require "luci.template"
756 local http = require "luci.http"
758 local config = self.config or {}
759 local maps = cbi.load(self.model, ...)
763 for i, res in ipairs(maps) do
765 local cstate = res:parse()
766 if cstate and (not state or cstate < state) then
771 local function _resolve_path(path)
772 return type(path) == "table" and build_url(unpack(path)) or path
775 if config.on_valid_to and state and state > 0 and state < 2 then
776 http.redirect(_resolve_path(config.on_valid_to))
780 if config.on_changed_to and state and state > 1 then
781 http.redirect(_resolve_path(config.on_changed_to))
785 if config.on_success_to and state and state > 0 then
786 http.redirect(_resolve_path(config.on_success_to))
790 if config.state_handler then
791 if not config.state_handler(state, maps) then
796 http.header("X-CBI-State", state or 0)
798 if not config.noheader then
799 tpl.render("cbi/header", {state = state})
804 local applymap = false
805 local pageaction = true
806 local parsechain = { }
808 for i, res in ipairs(maps) do
809 if res.apply_needed and res.parsechain then
811 for _, c in ipairs(res.parsechain) do
812 parsechain[#parsechain+1] = c
818 redirect = redirect or res.redirect
821 if res.pageaction == false then
826 messages = messages or { }
827 messages[#messages+1] = res.message
831 for i, res in ipairs(maps) do
837 pageaction = pageaction,
838 parsechain = parsechain
842 if not config.nofooter then
843 tpl.render("cbi/footer", {
845 pageaction = pageaction,
848 autoapply = config.autoapply
853 function cbi(model, config)
856 post = { ["cbi.submit"] = "1" },
864 local function _arcombine(self, ...)
866 local target = #argv > 0 and self.targets[2] or self.targets[1]
867 setfenv(target.target, self.env)
868 target:target(unpack(argv))
871 function arcombine(trg1, trg2)
872 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
876 local function _form(self, ...)
877 local cbi = require "luci.cbi"
878 local tpl = require "luci.template"
879 local http = require "luci.http"
881 local maps = luci.cbi.load(self.model, ...)
884 for i, res in ipairs(maps) do
885 local cstate = res:parse()
886 if cstate and (not state or cstate < state) then
891 http.header("X-CBI-State", state or 0)
893 for i, res in ipairs(maps) do
902 post = { ["cbi.submit"] = "1" },
908 translate = i18n.translate
910 -- This function does not actually translate the given argument but
911 -- is used by build/i18n-scan.pl to find translatable entries.