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 k, v in pairs(context.urltoken) do
33 url[#url+1] = http.urlencode(k)
35 url[#url+1] = http.urlencode(v)
39 for _, p in ipairs(path) do
40 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
46 return table.concat(url, "")
49 function node_visible(node)
52 (not node.title or #node.title == 0) or
53 (not node.target or node.hidden == true) or
54 (type(node.target) == "table" and node.target.type == "firstchild" and
55 (type(node.nodes) ~= "table" or not next(node.nodes)))
61 function node_childs(node)
65 for k, v in util.spairs(node.nodes,
67 return (node.nodes[a].order or 100)
68 < (node.nodes[b].order or 100)
71 if node_visible(v) then
80 function error404(message)
81 http.status(404, "Not Found")
82 message = message or "Not Found"
84 require("luci.template")
85 if not util.copcall(luci.template.render, "error404") then
86 http.prepare_content("text/plain")
92 function error500(message)
94 if not context.template_header_sent then
95 http.status(500, "Internal Server Error")
96 http.prepare_content("text/plain")
99 require("luci.template")
100 if not util.copcall(luci.template.render, "error500", {message=message}) then
101 http.prepare_content("text/plain")
108 function authenticator.htmlauth(validator, accs, default)
109 local user = http.formvalue("luci_username")
110 local pass = http.formvalue("luci_password")
112 if user and validator(user, pass) then
117 require("luci.template")
119 http.status(403, "Forbidden")
120 luci.template.render("sysauth", {duser=default, fuser=user})
126 function httpdispatch(request, prefix)
127 http.context.request = request
131 context.urltoken = {}
133 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
136 for _, node in ipairs(prefix) do
141 for node in pathinfo:gmatch("[^/]+") do
145 local stat, err = util.coxpcall(function()
146 dispatch(context.request)
151 --context._disable_memtrace()
154 local function require_post_security(target)
155 if type(target) == "table" then
156 if type(target.post) == "table" then
157 local param_name, required_val, request_val
159 for param_name, required_val in pairs(target.post) do
160 request_val = http.formvalue(param_name)
162 if (type(required_val) == "string" and
163 request_val ~= required_val) or
164 (required_val == true and
165 (request_val == nil or request_val == ""))
174 return (target.post == true)
180 function dispatch(request)
181 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
185 local conf = require "luci.config"
187 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
189 local lang = conf.main.lang or "auto"
190 if lang == "auto" then
191 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
192 for lpat in aclang:gmatch("[%w-]+") do
193 lpat = lpat and lpat:gsub("-", "_")
194 if conf.languages[lpat] then
200 require "luci.i18n".setlanguage(lang)
211 ctx.requestargs = ctx.requestargs or args
213 local token = ctx.urltoken
217 for i, s in ipairs(request) do
226 util.update(track, c)
234 for j=n+1, #request do
235 args[#args+1] = request[j]
236 freq[#freq+1] = request[j]
240 ctx.requestpath = ctx.requestpath or freq
244 i18n.loadc(track.i18n)
247 -- Init template engine
248 if (c and c.index) or not track.notemplate then
249 local tpl = require("luci.template")
250 local media = track.mediaurlbase or luci.config.main.mediaurlbase
251 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
253 for name, theme in pairs(luci.config.themes) do
254 if name:sub(1,1) ~= "." and pcall(tpl.Template,
255 "themes/%s/header" % fs.basename(theme)) then
259 assert(media, "No valid theme found")
262 local function _ifattr(cond, key, val)
264 local env = getfenv(3)
265 local scope = (type(env.self) == "table") and env.self
266 return string.format(
267 ' %s="%s"', tostring(key),
268 util.pcdata(tostring( val
269 or (type(env[key]) ~= "function" and env[key])
270 or (scope and type(scope[key]) ~= "function" and scope[key])
278 tpl.context.viewns = setmetatable({
280 include = function(name) tpl.Template(name):render(getfenv(2)) end;
281 translate = i18n.translate;
282 translatef = i18n.translatef;
283 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
284 striptags = util.striptags;
285 pcdata = util.pcdata;
287 theme = fs.basename(media);
288 resource = luci.config.main.resourcebase;
289 ifattr = function(...) return _ifattr(...) end;
290 attr = function(...) return _ifattr(true, ...) end;
292 }, {__index=function(table, key)
293 if key == "controller" then
295 elseif key == "REQUEST_URI" then
296 return build_url(unpack(ctx.requestpath))
297 elseif key == "token" then
300 return rawget(table, key) or _G[key]
305 track.dependent = (track.dependent ~= false)
306 assert(not track.dependent or not track.auto,
307 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
308 "has no parent node so the access to this location has been denied.\n" ..
309 "This is a software bug, please report this message at " ..
310 "http://luci.subsignal.org/trac/newticket"
313 if track.sysauth then
314 local authen = type(track.sysauth_authenticator) == "function"
315 and track.sysauth_authenticator
316 or authenticator[track.sysauth_authenticator]
318 local def = (type(track.sysauth) == "string") and track.sysauth
319 local accs = def and {track.sysauth} or track.sysauth
320 local sess = ctx.authsession
322 sess = http.getcookie("sysauth")
323 sess = sess and sess:match("^[a-f0-9]*$")
326 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
333 local eu = http.getenv("HTTP_AUTH_USER")
334 local ep = http.getenv("HTTP_AUTH_PASS")
335 if eu and ep and sys.user.checkpasswd(eu, ep) then
336 authen = function() return eu end
340 if not util.contains(accs, user) then
342 local user, sess = authen(sys.user.checkpasswd, accs, def)
344 if not user or not util.contains(accs, user) then
348 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
350 token = sys.uniqueid(16)
351 util.ubus("session", "set", {
352 ubus_rpc_session = sdat.ubus_rpc_session,
356 section = sys.uniqueid(16)
359 sess = sdat.ubus_rpc_session
363 if sess and token then
364 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
368 ctx.authsession = sess
369 ctx.authtoken = token
372 http.redirect(build_url(unpack(ctx.requestpath)))
376 http.status(403, "Forbidden")
380 ctx.authsession = sess
381 ctx.authtoken = token
386 if c and require_post_security(c.target) then
387 if http.getenv("REQUEST_METHOD") ~= "POST" then
388 http.status(405, "Method Not Allowed")
389 http.header("Allow", "POST")
393 if http.formvalue("token") ~= ctx.authtoken then
394 http.status(403, "Forbidden")
395 luci.template.render("csrftoken")
400 if track.setgroup then
401 sys.process.setgroup(track.setgroup)
404 if track.setuser then
405 -- trigger ubus connection before dropping root privs
408 sys.process.setuser(track.setuser)
413 if type(c.target) == "function" then
415 elseif type(c.target) == "table" then
416 target = c.target.target
420 if c and (c.index or type(target) == "function") then
422 ctx.requested = ctx.requested or ctx.dispatched
425 if c and c.index then
426 local tpl = require "luci.template"
428 if util.copcall(tpl.render, "indexer", {}) then
433 if type(target) == "function" then
434 util.copcall(function()
435 local oldenv = getfenv(target)
436 local module = require(c.module)
437 local env = setmetatable({}, {__index=
440 return rawget(tbl, key) or module[key] or oldenv[key]
447 if type(c.target) == "table" then
448 ok, err = util.copcall(target, c.target, unpack(args))
450 ok, err = util.copcall(target, unpack(args))
453 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
454 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
455 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
458 if not root or not root.target then
459 error404("No root node was registered, this usually happens if no module was installed.\n" ..
460 "Install luci-mod-admin-full and retry. " ..
461 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
463 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
464 "If this url belongs to an extension, make sure it is properly installed.\n" ..
465 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
470 function createindex()
471 local controllers = { }
472 local base = "%s/controller/" % util.libpath()
475 for path in (fs.glob("%s*.lua" % base) or function() end) do
476 controllers[#controllers+1] = path
479 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
480 controllers[#controllers+1] = path
484 local cachedate = fs.stat(indexcache, "mtime")
487 for _, obj in ipairs(controllers) do
488 local omtime = fs.stat(obj, "mtime")
489 realdate = (omtime and omtime > realdate) and omtime or realdate
492 if cachedate > realdate and sys.process.info("uid") == 0 then
494 sys.process.info("uid") == fs.stat(indexcache, "uid")
495 and fs.stat(indexcache, "modestr") == "rw-------",
496 "Fatal: Indexcache is not sane!"
499 index = loadfile(indexcache)()
507 for _, path in ipairs(controllers) do
508 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
509 local mod = require(modname)
511 "Invalid controller file found\n" ..
512 "The file '" .. path .. "' contains an invalid module line.\n" ..
513 "Please verify whether the module name is set to '" .. modname ..
514 "' - It must correspond to the file path!")
516 local idx = mod.index
517 assert(type(idx) == "function",
518 "Invalid controller file found\n" ..
519 "The file '" .. path .. "' contains no index() function.\n" ..
520 "Please make sure that the controller contains a valid " ..
521 "index function and verify the spelling!")
527 local f = nixio.open(indexcache, "w", 600)
528 f:writeall(util.get_bytecode(index))
533 -- Build the index before if it does not exist yet.
534 function createtree()
540 local tree = {nodes={}, inreq=true}
543 ctx.treecache = setmetatable({}, {__mode="v"})
547 -- Load default translation
548 require "luci.i18n".loadc("base")
550 local scope = setmetatable({}, {__index = luci.dispatcher})
552 for k, v in pairs(index) do
558 local function modisort(a,b)
559 return modi[a].order < modi[b].order
562 for _, v in util.spairs(modi, modisort) do
563 scope._NAME = v.module
564 setfenv(v.func, scope)
571 function modifier(func, order)
572 context.modifiers[#context.modifiers+1] = {
580 function assign(path, clone, title, order)
581 local obj = node(unpack(path))
588 setmetatable(obj, {__index = _create_node(clone)})
593 function entry(path, target, title, order)
594 local c = node(unpack(path))
599 c.module = getfenv(2)._NAME
604 -- enabling the node.
606 return _create_node({...})
610 local c = _create_node({...})
612 c.module = getfenv(2)._NAME
618 function _create_node(path)
623 local name = table.concat(path, ".")
624 local c = context.treecache[name]
627 local last = table.remove(path)
628 local parent = _create_node(path)
630 c = {nodes={}, auto=true}
631 -- the node is "in request" if the request path matches
632 -- at least up to the length of the node path
633 if parent.inreq and context.path[#path+1] == last then
636 parent.nodes[last] = c
637 context.treecache[name] = c
644 function _firstchild()
645 local path = { unpack(context.path) }
646 local name = table.concat(path, ".")
647 local node = context.treecache[name]
650 if node and node.nodes and next(node.nodes) then
652 for k, v in pairs(node.nodes) do
654 (v.order or 100) < (node.nodes[lowest].order or 100)
661 assert(lowest ~= nil,
662 "The requested node contains no childs, unable to redispatch")
664 path[#path+1] = lowest
668 function firstchild()
669 return { type = "firstchild", target = _firstchild }
675 for _, r in ipairs({...}) do
683 function rewrite(n, ...)
686 local dispatched = util.clone(context.dispatched)
689 table.remove(dispatched, 1)
692 for i, r in ipairs(req) do
693 table.insert(dispatched, i, r)
696 for _, r in ipairs({...}) do
697 dispatched[#dispatched+1] = r
705 local function _call(self, ...)
706 local func = getfenv()[self.name]
708 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
710 assert(type(func) == "function",
711 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
712 'of type "' .. type(func) .. '".')
714 if #self.argv > 0 then
715 return func(unpack(self.argv), ...)
721 function call(name, ...)
722 return {type = "call", argv = {...}, name = name, target = _call}
725 function post_on(params, name, ...)
736 return post_on(true, ...)
740 local _template = function(self, ...)
741 require "luci.template".render(self.view)
744 function template(name)
745 return {type = "template", view = name, target = _template}
749 local function _cbi(self, ...)
750 local cbi = require "luci.cbi"
751 local tpl = require "luci.template"
752 local http = require "luci.http"
754 local config = self.config or {}
755 local maps = cbi.load(self.model, ...)
759 for i, res in ipairs(maps) do
761 local cstate = res:parse()
762 if cstate and (not state or cstate < state) then
767 local function _resolve_path(path)
768 return type(path) == "table" and build_url(unpack(path)) or path
771 if config.on_valid_to and state and state > 0 and state < 2 then
772 http.redirect(_resolve_path(config.on_valid_to))
776 if config.on_changed_to and state and state > 1 then
777 http.redirect(_resolve_path(config.on_changed_to))
781 if config.on_success_to and state and state > 0 then
782 http.redirect(_resolve_path(config.on_success_to))
786 if config.state_handler then
787 if not config.state_handler(state, maps) then
792 http.header("X-CBI-State", state or 0)
794 if not config.noheader then
795 tpl.render("cbi/header", {state = state})
800 local applymap = false
801 local pageaction = true
802 local parsechain = { }
804 for i, res in ipairs(maps) do
805 if res.apply_needed and res.parsechain then
807 for _, c in ipairs(res.parsechain) do
808 parsechain[#parsechain+1] = c
814 redirect = redirect or res.redirect
817 if res.pageaction == false then
822 messages = messages or { }
823 messages[#messages+1] = res.message
827 for i, res in ipairs(maps) do
833 pageaction = pageaction,
834 parsechain = parsechain
838 if not config.nofooter then
839 tpl.render("cbi/footer", {
841 pageaction = pageaction,
844 autoapply = config.autoapply
849 function cbi(model, config)
852 post = { ["cbi.submit"] = "1" },
860 local function _arcombine(self, ...)
862 local target = #argv > 0 and self.targets[2] or self.targets[1]
863 setfenv(target.target, self.env)
864 target:target(unpack(argv))
867 function arcombine(trg1, trg2)
868 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
872 local function _form(self, ...)
873 local cbi = require "luci.cbi"
874 local tpl = require "luci.template"
875 local http = require "luci.http"
877 local maps = luci.cbi.load(self.model, ...)
880 for i, res in ipairs(maps) do
881 local cstate = res:parse()
882 if cstate and (not state or cstate < state) then
887 http.header("X-CBI-State", state or 0)
889 for i, res in ipairs(maps) do
898 post = { ["cbi.submit"] = "1" },
904 translate = i18n.translate
906 -- This function does not actually translate the given argument but
907 -- is used by build/i18n-scan.pl to find translatable entries.