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
116 if context.urltoken.stok then
117 context.urltoken.stok = nil
119 local cookie = 'sysauth=%s; expires=%s; path=%s/' %{
120 http.getcookie('sysauth') or 'x',
121 'Thu, 01 Jan 1970 01:00:00 GMT',
125 http.header("Set-Cookie", cookie)
126 http.redirect(build_url())
129 require("luci.template")
131 http.status(403, "Forbidden")
132 luci.template.render("sysauth", {duser=default, fuser=user})
139 function httpdispatch(request, prefix)
140 http.context.request = request
144 context.urltoken = {}
146 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
149 for _, node in ipairs(prefix) do
154 local tokensok = true
155 for node in pathinfo:gmatch("[^/]+") do
158 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
161 context.urltoken[tkey] = tval
168 local stat, err = util.coxpcall(function()
169 dispatch(context.request)
174 --context._disable_memtrace()
177 local function require_post_security(target)
178 if type(target) == "table" then
179 if type(target.post) == "table" then
180 local param_name, required_val, request_val
182 for param_name, required_val in pairs(target.post) do
183 request_val = http.formvalue(param_name)
185 if (type(required_val) == "string" and
186 request_val ~= required_val) or
187 (required_val == true and
188 (request_val == nil or request_val == ""))
197 return (target.post == true)
203 function dispatch(request)
204 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
208 local conf = require "luci.config"
210 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
212 local lang = conf.main.lang or "auto"
213 if lang == "auto" then
214 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
215 for lpat in aclang:gmatch("[%w-]+") do
216 lpat = lpat and lpat:gsub("-", "_")
217 if conf.languages[lpat] then
223 require "luci.i18n".setlanguage(lang)
234 ctx.requestargs = ctx.requestargs or args
236 local token = ctx.urltoken
240 for i, s in ipairs(request) do
249 util.update(track, c)
257 for j=n+1, #request do
258 args[#args+1] = request[j]
259 freq[#freq+1] = request[j]
263 ctx.requestpath = ctx.requestpath or freq
267 i18n.loadc(track.i18n)
270 -- Init template engine
271 if (c and c.index) or not track.notemplate then
272 local tpl = require("luci.template")
273 local media = track.mediaurlbase or luci.config.main.mediaurlbase
274 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
276 for name, theme in pairs(luci.config.themes) do
277 if name:sub(1,1) ~= "." and pcall(tpl.Template,
278 "themes/%s/header" % fs.basename(theme)) then
282 assert(media, "No valid theme found")
285 local function _ifattr(cond, key, val)
287 local env = getfenv(3)
288 local scope = (type(env.self) == "table") and env.self
289 return string.format(
290 ' %s="%s"', tostring(key),
291 util.pcdata(tostring( val
292 or (type(env[key]) ~= "function" and env[key])
293 or (scope and type(scope[key]) ~= "function" and scope[key])
301 tpl.context.viewns = setmetatable({
303 include = function(name) tpl.Template(name):render(getfenv(2)) end;
304 translate = i18n.translate;
305 translatef = i18n.translatef;
306 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
307 striptags = util.striptags;
308 pcdata = util.pcdata;
310 theme = fs.basename(media);
311 resource = luci.config.main.resourcebase;
312 ifattr = function(...) return _ifattr(...) end;
313 attr = function(...) return _ifattr(true, ...) end;
314 token = ctx.urltoken.stok;
316 }, {__index=function(table, key)
317 if key == "controller" then
319 elseif key == "REQUEST_URI" then
320 return build_url(unpack(ctx.requestpath))
322 return rawget(table, key) or _G[key]
327 track.dependent = (track.dependent ~= false)
328 assert(not track.dependent or not track.auto,
329 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
330 "has no parent node so the access to this location has been denied.\n" ..
331 "This is a software bug, please report this message at " ..
332 "http://luci.subsignal.org/trac/newticket"
335 if track.sysauth then
336 local authen = type(track.sysauth_authenticator) == "function"
337 and track.sysauth_authenticator
338 or authenticator[track.sysauth_authenticator]
340 local def = (type(track.sysauth) == "string") and track.sysauth
341 local accs = def and {track.sysauth} or track.sysauth
342 local sess = ctx.authsession
343 local verifytoken = false
345 sess = http.getcookie("sysauth")
346 sess = sess and sess:match("^[a-f0-9]*$")
350 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
354 if not verifytoken or ctx.urltoken.stok == sdat.token then
358 local eu = http.getenv("HTTP_AUTH_USER")
359 local ep = http.getenv("HTTP_AUTH_PASS")
360 if eu and ep and sys.user.checkpasswd(eu, ep) then
361 authen = function() return eu end
365 if not util.contains(accs, user) then
367 local user, sess = authen(sys.user.checkpasswd, accs, def)
369 if not user or not util.contains(accs, user) then
373 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
375 token = sys.uniqueid(16)
376 util.ubus("session", "set", {
377 ubus_rpc_session = sdat.ubus_rpc_session,
381 section = sys.uniqueid(16)
384 sess = sdat.ubus_rpc_session
388 if sess and token then
389 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
393 ctx.urltoken.stok = token
394 ctx.authsession = sess
397 http.redirect(build_url(unpack(ctx.requestpath)))
401 http.status(403, "Forbidden")
405 ctx.authsession = sess
410 if c and require_post_security(c.target) then
411 if http.getenv("REQUEST_METHOD") ~= "POST" then
412 http.status(405, "Method Not Allowed")
413 http.header("Allow", "POST")
417 if http.formvalue("token") ~= ctx.urltoken.stok then
418 http.status(403, "Forbidden")
419 luci.template.render("csrftoken")
424 if track.setgroup then
425 sys.process.setgroup(track.setgroup)
428 if track.setuser then
429 -- trigger ubus connection before dropping root privs
432 sys.process.setuser(track.setuser)
437 if type(c.target) == "function" then
439 elseif type(c.target) == "table" then
440 target = c.target.target
444 if c and (c.index or type(target) == "function") then
446 ctx.requested = ctx.requested or ctx.dispatched
449 if c and c.index then
450 local tpl = require "luci.template"
452 if util.copcall(tpl.render, "indexer", {}) then
457 if type(target) == "function" then
458 util.copcall(function()
459 local oldenv = getfenv(target)
460 local module = require(c.module)
461 local env = setmetatable({}, {__index=
464 return rawget(tbl, key) or module[key] or oldenv[key]
471 if type(c.target) == "table" then
472 ok, err = util.copcall(target, c.target, unpack(args))
474 ok, err = util.copcall(target, unpack(args))
477 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
478 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
479 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
482 if not root or not root.target then
483 error404("No root node was registered, this usually happens if no module was installed.\n" ..
484 "Install luci-mod-admin-full and retry. " ..
485 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
487 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
488 "If this url belongs to an extension, make sure it is properly installed.\n" ..
489 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
494 function createindex()
495 local controllers = { }
496 local base = "%s/controller/" % util.libpath()
499 for path in (fs.glob("%s*.lua" % base) or function() end) do
500 controllers[#controllers+1] = path
503 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
504 controllers[#controllers+1] = path
508 local cachedate = fs.stat(indexcache, "mtime")
511 for _, obj in ipairs(controllers) do
512 local omtime = fs.stat(obj, "mtime")
513 realdate = (omtime and omtime > realdate) and omtime or realdate
516 if cachedate > realdate and sys.process.info("uid") == 0 then
518 sys.process.info("uid") == fs.stat(indexcache, "uid")
519 and fs.stat(indexcache, "modestr") == "rw-------",
520 "Fatal: Indexcache is not sane!"
523 index = loadfile(indexcache)()
531 for _, path in ipairs(controllers) do
532 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
533 local mod = require(modname)
535 "Invalid controller file found\n" ..
536 "The file '" .. path .. "' contains an invalid module line.\n" ..
537 "Please verify whether the module name is set to '" .. modname ..
538 "' - It must correspond to the file path!")
540 local idx = mod.index
541 assert(type(idx) == "function",
542 "Invalid controller file found\n" ..
543 "The file '" .. path .. "' contains no index() function.\n" ..
544 "Please make sure that the controller contains a valid " ..
545 "index function and verify the spelling!")
551 local f = nixio.open(indexcache, "w", 600)
552 f:writeall(util.get_bytecode(index))
557 -- Build the index before if it does not exist yet.
558 function createtree()
564 local tree = {nodes={}, inreq=true}
567 ctx.treecache = setmetatable({}, {__mode="v"})
571 -- Load default translation
572 require "luci.i18n".loadc("base")
574 local scope = setmetatable({}, {__index = luci.dispatcher})
576 for k, v in pairs(index) do
582 local function modisort(a,b)
583 return modi[a].order < modi[b].order
586 for _, v in util.spairs(modi, modisort) do
587 scope._NAME = v.module
588 setfenv(v.func, scope)
595 function modifier(func, order)
596 context.modifiers[#context.modifiers+1] = {
604 function assign(path, clone, title, order)
605 local obj = node(unpack(path))
612 setmetatable(obj, {__index = _create_node(clone)})
617 function entry(path, target, title, order)
618 local c = node(unpack(path))
623 c.module = getfenv(2)._NAME
628 -- enabling the node.
630 return _create_node({...})
634 local c = _create_node({...})
636 c.module = getfenv(2)._NAME
642 function _create_node(path)
647 local name = table.concat(path, ".")
648 local c = context.treecache[name]
651 local last = table.remove(path)
652 local parent = _create_node(path)
654 c = {nodes={}, auto=true}
655 -- the node is "in request" if the request path matches
656 -- at least up to the length of the node path
657 if parent.inreq and context.path[#path+1] == last then
660 parent.nodes[last] = c
661 context.treecache[name] = c
668 function _firstchild()
669 local path = { unpack(context.path) }
670 local name = table.concat(path, ".")
671 local node = context.treecache[name]
674 if node and node.nodes and next(node.nodes) then
676 for k, v in pairs(node.nodes) do
678 (v.order or 100) < (node.nodes[lowest].order or 100)
685 assert(lowest ~= nil,
686 "The requested node contains no childs, unable to redispatch")
688 path[#path+1] = lowest
692 function firstchild()
693 return { type = "firstchild", target = _firstchild }
699 for _, r in ipairs({...}) do
707 function rewrite(n, ...)
710 local dispatched = util.clone(context.dispatched)
713 table.remove(dispatched, 1)
716 for i, r in ipairs(req) do
717 table.insert(dispatched, i, r)
720 for _, r in ipairs({...}) do
721 dispatched[#dispatched+1] = r
729 local function _call(self, ...)
730 local func = getfenv()[self.name]
732 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
734 assert(type(func) == "function",
735 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
736 'of type "' .. type(func) .. '".')
738 if #self.argv > 0 then
739 return func(unpack(self.argv), ...)
745 function call(name, ...)
746 return {type = "call", argv = {...}, name = name, target = _call}
749 function post_on(params, name, ...)
760 return post_on(true, ...)
764 local _template = function(self, ...)
765 require "luci.template".render(self.view)
768 function template(name)
769 return {type = "template", view = name, target = _template}
773 local function _cbi(self, ...)
774 local cbi = require "luci.cbi"
775 local tpl = require "luci.template"
776 local http = require "luci.http"
778 local config = self.config or {}
779 local maps = cbi.load(self.model, ...)
783 for i, res in ipairs(maps) do
785 local cstate = res:parse()
786 if cstate and (not state or cstate < state) then
791 local function _resolve_path(path)
792 return type(path) == "table" and build_url(unpack(path)) or path
795 if config.on_valid_to and state and state > 0 and state < 2 then
796 http.redirect(_resolve_path(config.on_valid_to))
800 if config.on_changed_to and state and state > 1 then
801 http.redirect(_resolve_path(config.on_changed_to))
805 if config.on_success_to and state and state > 0 then
806 http.redirect(_resolve_path(config.on_success_to))
810 if config.state_handler then
811 if not config.state_handler(state, maps) then
816 http.header("X-CBI-State", state or 0)
818 if not config.noheader then
819 tpl.render("cbi/header", {state = state})
824 local applymap = false
825 local pageaction = true
826 local parsechain = { }
828 for i, res in ipairs(maps) do
829 if res.apply_needed and res.parsechain then
831 for _, c in ipairs(res.parsechain) do
832 parsechain[#parsechain+1] = c
838 redirect = redirect or res.redirect
841 if res.pageaction == false then
846 messages = messages or { }
847 messages[#messages+1] = res.message
851 for i, res in ipairs(maps) do
857 pageaction = pageaction,
858 parsechain = parsechain
862 if not config.nofooter then
863 tpl.render("cbi/footer", {
865 pageaction = pageaction,
868 autoapply = config.autoapply
873 function cbi(model, config)
876 post = { ["cbi.submit"] = "1" },
884 local function _arcombine(self, ...)
886 local target = #argv > 0 and self.targets[2] or self.targets[1]
887 setfenv(target.target, self.env)
888 target:target(unpack(argv))
891 function arcombine(trg1, trg2)
892 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
896 local function _form(self, ...)
897 local cbi = require "luci.cbi"
898 local tpl = require "luci.template"
899 local http = require "luci.http"
901 local maps = luci.cbi.load(self.model, ...)
904 for i, res in ipairs(maps) do
905 local cstate = res:parse()
906 if cstate and (not state or cstate < state) then
911 http.header("X-CBI-State", state or 0)
913 for i, res in ipairs(maps) do
922 post = { ["cbi.submit"] = "1" },
928 translate = i18n.translate
930 -- This function does not actually translate the given argument but
931 -- is used by build/i18n-scan.pl to find translatable entries.