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 i18n = require "luci.i18n"
201 local lang = conf.main.lang or "auto"
202 if lang == "auto" then
203 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
204 for lpat in aclang:gmatch("[%w-]+") do
205 lpat = lpat and lpat:gsub("-", "_")
206 if conf.languages[lpat] then
212 if lang == "auto" then
215 i18n.setlanguage(lang)
226 ctx.requestargs = ctx.requestargs or args
231 for i, s in ipairs(request) do
240 util.update(track, c)
248 for j=n+1, #request do
249 args[#args+1] = request[j]
250 freq[#freq+1] = request[j]
254 ctx.requestpath = ctx.requestpath or freq
258 i18n.loadc(track.i18n)
261 -- Init template engine
262 if (c and c.index) or not track.notemplate then
263 local tpl = require("luci.template")
264 local media = track.mediaurlbase or luci.config.main.mediaurlbase
265 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
267 for name, theme in pairs(luci.config.themes) do
268 if name:sub(1,1) ~= "." and pcall(tpl.Template,
269 "themes/%s/header" % fs.basename(theme)) then
273 assert(media, "No valid theme found")
276 local function _ifattr(cond, key, val)
278 local env = getfenv(3)
279 local scope = (type(env.self) == "table") and env.self
280 if type(val) == "table" then
281 if not next(val) then
284 val = util.serialize_json(val)
287 return string.format(
288 ' %s="%s"', tostring(key),
289 util.pcdata(tostring( val
290 or (type(env[key]) ~= "function" and env[key])
291 or (scope and type(scope[key]) ~= "function" and scope[key])
299 tpl.context.viewns = setmetatable({
301 include = function(name) tpl.Template(name):render(getfenv(2)) end;
302 translate = i18n.translate;
303 translatef = i18n.translatef;
304 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
305 striptags = util.striptags;
306 pcdata = util.pcdata;
308 theme = fs.basename(media);
309 resource = luci.config.main.resourcebase;
310 ifattr = function(...) return _ifattr(...) end;
311 attr = function(...) return _ifattr(true, ...) end;
313 }, {__index=function(table, key)
314 if key == "controller" then
316 elseif key == "REQUEST_URI" then
317 return build_url(unpack(ctx.requestpath))
318 elseif key == "token" then
321 return rawget(table, key) or _G[key]
326 track.dependent = (track.dependent ~= false)
327 assert(not track.dependent or not track.auto,
328 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
329 "has no parent node so the access to this location has been denied.\n" ..
330 "This is a software bug, please report this message at " ..
331 "https://github.com/openwrt/luci/issues"
334 if track.sysauth then
335 local authen = type(track.sysauth_authenticator) == "function"
336 and track.sysauth_authenticator
337 or authenticator[track.sysauth_authenticator]
339 local def = (type(track.sysauth) == "string") and track.sysauth
340 local accs = def and {track.sysauth} or track.sysauth
341 local sess = ctx.authsession
343 sess = http.getcookie("sysauth")
344 sess = sess and sess:match("^[a-f0-9]*$")
347 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
354 local eu = http.getenv("HTTP_AUTH_USER")
355 local ep = http.getenv("HTTP_AUTH_PASS")
356 if eu and ep and sys.user.checkpasswd(eu, ep) then
357 authen = function() return eu end
361 if not util.contains(accs, user) then
363 local user, sess = authen(sys.user.checkpasswd, accs, def)
365 if not user or not util.contains(accs, user) then
369 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
371 token = sys.uniqueid(16)
372 util.ubus("session", "set", {
373 ubus_rpc_session = sdat.ubus_rpc_session,
377 section = sys.uniqueid(16)
380 sess = sdat.ubus_rpc_session
384 if sess and token then
385 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
387 ctx.authsession = sess
388 ctx.authtoken = token
391 http.redirect(build_url(unpack(ctx.requestpath)))
395 http.status(403, "Forbidden")
399 ctx.authsession = sess
400 ctx.authtoken = token
405 if c and require_post_security(c.target) then
406 if not test_post_security(c) then
411 if track.setgroup then
412 sys.process.setgroup(track.setgroup)
415 if track.setuser then
416 sys.process.setuser(track.setuser)
421 if type(c.target) == "function" then
423 elseif type(c.target) == "table" then
424 target = c.target.target
428 if c and (c.index or type(target) == "function") then
430 ctx.requested = ctx.requested or ctx.dispatched
433 if c and c.index then
434 local tpl = require "luci.template"
436 if util.copcall(tpl.render, "indexer", {}) then
441 if type(target) == "function" then
442 util.copcall(function()
443 local oldenv = getfenv(target)
444 local module = require(c.module)
445 local env = setmetatable({}, {__index=
448 return rawget(tbl, key) or module[key] or oldenv[key]
455 if type(c.target) == "table" then
456 ok, err = util.copcall(target, c.target, unpack(args))
458 ok, err = util.copcall(target, unpack(args))
461 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
462 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
463 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
466 if not root or not root.target then
467 error404("No root node was registered, this usually happens if no module was installed.\n" ..
468 "Install luci-mod-admin-full and retry. " ..
469 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
471 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
472 "If this url belongs to an extension, make sure it is properly installed.\n" ..
473 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
478 function createindex()
479 local controllers = { }
480 local base = "%s/controller/" % util.libpath()
483 for path in (fs.glob("%s*.lua" % base) or function() end) do
484 controllers[#controllers+1] = path
487 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
488 controllers[#controllers+1] = path
492 local cachedate = fs.stat(indexcache, "mtime")
495 for _, obj in ipairs(controllers) do
496 local omtime = fs.stat(obj, "mtime")
497 realdate = (omtime and omtime > realdate) and omtime or realdate
500 if cachedate > realdate and sys.process.info("uid") == 0 then
502 sys.process.info("uid") == fs.stat(indexcache, "uid")
503 and fs.stat(indexcache, "modestr") == "rw-------",
504 "Fatal: Indexcache is not sane!"
507 index = loadfile(indexcache)()
515 for _, path in ipairs(controllers) do
516 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
517 local mod = require(modname)
519 "Invalid controller file found\n" ..
520 "The file '" .. path .. "' contains an invalid module line.\n" ..
521 "Please verify whether the module name is set to '" .. modname ..
522 "' - It must correspond to the file path!")
524 local idx = mod.index
525 assert(type(idx) == "function",
526 "Invalid controller file found\n" ..
527 "The file '" .. path .. "' contains no index() function.\n" ..
528 "Please make sure that the controller contains a valid " ..
529 "index function and verify the spelling!")
535 local f = nixio.open(indexcache, "w", 600)
536 f:writeall(util.get_bytecode(index))
541 -- Build the index before if it does not exist yet.
542 function createtree()
548 local tree = {nodes={}, inreq=true}
551 ctx.treecache = setmetatable({}, {__mode="v"})
555 -- Load default translation
556 require "luci.i18n".loadc("base")
558 local scope = setmetatable({}, {__index = luci.dispatcher})
560 for k, v in pairs(index) do
566 local function modisort(a,b)
567 return modi[a].order < modi[b].order
570 for _, v in util.spairs(modi, modisort) do
571 scope._NAME = v.module
572 setfenv(v.func, scope)
579 function modifier(func, order)
580 context.modifiers[#context.modifiers+1] = {
588 function assign(path, clone, title, order)
589 local obj = node(unpack(path))
596 setmetatable(obj, {__index = _create_node(clone)})
601 function entry(path, target, title, order)
602 local c = node(unpack(path))
607 c.module = getfenv(2)._NAME
612 -- enabling the node.
614 return _create_node({...})
618 local c = _create_node({...})
620 c.module = getfenv(2)._NAME
626 function _create_node(path)
631 local name = table.concat(path, ".")
632 local c = context.treecache[name]
635 local last = table.remove(path)
636 local parent = _create_node(path)
638 c = {nodes={}, auto=true}
639 -- the node is "in request" if the request path matches
640 -- at least up to the length of the node path
641 if parent.inreq and context.path[#path+1] == last then
644 parent.nodes[last] = c
645 context.treecache[name] = c
652 function _firstchild()
653 local path = { unpack(context.path) }
654 local name = table.concat(path, ".")
655 local node = context.treecache[name]
658 if node and node.nodes and next(node.nodes) then
660 for k, v in pairs(node.nodes) do
662 (v.order or 100) < (node.nodes[lowest].order or 100)
669 assert(lowest ~= nil,
670 "The requested node contains no childs, unable to redispatch")
672 path[#path+1] = lowest
676 function firstchild()
677 return { type = "firstchild", target = _firstchild }
683 for _, r in ipairs({...}) do
691 function rewrite(n, ...)
694 local dispatched = util.clone(context.dispatched)
697 table.remove(dispatched, 1)
700 for i, r in ipairs(req) do
701 table.insert(dispatched, i, r)
704 for _, r in ipairs({...}) do
705 dispatched[#dispatched+1] = r
713 local function _call(self, ...)
714 local func = getfenv()[self.name]
716 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
718 assert(type(func) == "function",
719 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
720 'of type "' .. type(func) .. '".')
722 if #self.argv > 0 then
723 return func(unpack(self.argv), ...)
729 function call(name, ...)
730 return {type = "call", argv = {...}, name = name, target = _call}
733 function post_on(params, name, ...)
744 return post_on(true, ...)
748 local _template = function(self, ...)
749 require "luci.template".render(self.view)
752 function template(name)
753 return {type = "template", view = name, target = _template}
757 local function _cbi(self, ...)
758 local cbi = require "luci.cbi"
759 local tpl = require "luci.template"
760 local http = require "luci.http"
762 local config = self.config or {}
763 local maps = cbi.load(self.model, ...)
767 for i, res in ipairs(maps) do
769 local cstate = res:parse()
770 if cstate and (not state or cstate < state) then
775 local function _resolve_path(path)
776 return type(path) == "table" and build_url(unpack(path)) or path
779 if config.on_valid_to and state and state > 0 and state < 2 then
780 http.redirect(_resolve_path(config.on_valid_to))
784 if config.on_changed_to and state and state > 1 then
785 http.redirect(_resolve_path(config.on_changed_to))
789 if config.on_success_to and state and state > 0 then
790 http.redirect(_resolve_path(config.on_success_to))
794 if config.state_handler then
795 if not config.state_handler(state, maps) then
800 http.header("X-CBI-State", state or 0)
802 if not config.noheader then
803 tpl.render("cbi/header", {state = state})
808 local applymap = false
809 local pageaction = true
810 local parsechain = { }
812 for i, res in ipairs(maps) do
813 if res.apply_needed and res.parsechain then
815 for _, c in ipairs(res.parsechain) do
816 parsechain[#parsechain+1] = c
822 redirect = redirect or res.redirect
825 if res.pageaction == false then
830 messages = messages or { }
831 messages[#messages+1] = res.message
835 for i, res in ipairs(maps) do
841 pageaction = pageaction,
842 parsechain = parsechain
846 if not config.nofooter then
847 tpl.render("cbi/footer", {
849 pageaction = pageaction,
852 autoapply = config.autoapply
857 function cbi(model, config)
860 post = { ["cbi.submit"] = "1" },
868 local function _arcombine(self, ...)
870 local target = #argv > 0 and self.targets[2] or self.targets[1]
871 setfenv(target.target, self.env)
872 target:target(unpack(argv))
875 function arcombine(trg1, trg2)
876 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
880 local function _form(self, ...)
881 local cbi = require "luci.cbi"
882 local tpl = require "luci.template"
883 local http = require "luci.http"
885 local maps = luci.cbi.load(self.model, ...)
888 for i, res in ipairs(maps) do
889 local cstate = res:parse()
890 if cstate and (not state or cstate < state) then
895 http.header("X-CBI-State", state or 0)
897 for i, res in ipairs(maps) do
906 post = { ["cbi.submit"] = "1" },
912 translate = i18n.translate
914 -- This function does not actually translate the given argument but
915 -- is used by build/i18n-scan.pl to find translatable entries.