1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
4 local fs = require "nixio.fs"
5 local sys = require "luci.sys"
6 local util = require "luci.util"
7 local http = require "luci.http"
8 local nixio = require "nixio", require "nixio.util"
10 module("luci.dispatcher", package.seeall)
11 context = util.threadlocal()
12 uci = require "luci.model.uci"
13 i18n = require "luci.i18n"
25 function build_url(...)
27 local url = { http.getenv("SCRIPT_NAME") or "" }
30 for k, v in pairs(context.urltoken) do
32 url[#url+1] = http.urlencode(k)
34 url[#url+1] = http.urlencode(v)
38 for _, p in ipairs(path) do
39 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
45 return table.concat(url, "")
48 function node_visible(node)
51 (not node.title or #node.title == 0) or
52 (not node.target or node.hidden == true) or
53 (type(node.target) == "table" and node.target.type == "firstchild" and
54 (type(node.nodes) ~= "table" or not next(node.nodes)))
60 function node_childs(node)
64 for k, v in util.spairs(node.nodes,
66 return (node.nodes[a].order or 100)
67 < (node.nodes[b].order or 100)
70 if node_visible(v) then
79 function error404(message)
80 http.status(404, "Not Found")
81 message = message or "Not Found"
83 require("luci.template")
84 if not util.copcall(luci.template.render, "error404") then
85 http.prepare_content("text/plain")
91 function error500(message)
93 if not context.template_header_sent then
94 http.status(500, "Internal Server Error")
95 http.prepare_content("text/plain")
98 require("luci.template")
99 if not util.copcall(luci.template.render, "error500", {message=message}) then
100 http.prepare_content("text/plain")
107 function authenticator.htmlauth(validator, accs, default)
108 local user = http.formvalue("luci_username")
109 local pass = http.formvalue("luci_password")
111 if user and validator(user, pass) then
115 if context.urltoken.stok then
116 context.urltoken.stok = nil
118 local cookie = 'sysauth=%s; expires=%s; path=%s/' %{
119 http.getcookie('sysauth') or 'x',
120 'Thu, 01 Jan 1970 01:00:00 GMT',
124 http.header("Set-Cookie", cookie)
125 http.redirect(build_url())
128 require("luci.template")
130 http.status(403, "Forbidden")
131 luci.template.render("sysauth", {duser=default, fuser=user})
138 function httpdispatch(request, prefix)
139 http.context.request = request
143 context.urltoken = {}
145 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
148 for _, node in ipairs(prefix) do
153 local tokensok = true
154 for node in pathinfo:gmatch("[^/]+") do
157 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
160 context.urltoken[tkey] = tval
167 local stat, err = util.coxpcall(function()
168 dispatch(context.request)
173 --context._disable_memtrace()
176 function dispatch(request)
177 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
181 local conf = require "luci.config"
183 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
185 local lang = conf.main.lang or "auto"
186 if lang == "auto" then
187 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
188 for lpat in aclang:gmatch("[%w-]+") do
189 lpat = lpat and lpat:gsub("-", "_")
190 if conf.languages[lpat] then
196 require "luci.i18n".setlanguage(lang)
207 ctx.requestargs = ctx.requestargs or args
209 local token = ctx.urltoken
213 for i, s in ipairs(request) do
222 util.update(track, c)
230 for j=n+1, #request do
231 args[#args+1] = request[j]
232 freq[#freq+1] = request[j]
236 ctx.requestpath = ctx.requestpath or freq
240 i18n.loadc(track.i18n)
243 -- Init template engine
244 if (c and c.index) or not track.notemplate then
245 local tpl = require("luci.template")
246 local media = track.mediaurlbase or luci.config.main.mediaurlbase
247 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
249 for name, theme in pairs(luci.config.themes) do
250 if name:sub(1,1) ~= "." and pcall(tpl.Template,
251 "themes/%s/header" % fs.basename(theme)) then
255 assert(media, "No valid theme found")
258 local function _ifattr(cond, key, val)
260 local env = getfenv(3)
261 local scope = (type(env.self) == "table") and env.self
262 return string.format(
263 ' %s="%s"', tostring(key),
264 util.pcdata(tostring( val
265 or (type(env[key]) ~= "function" and env[key])
266 or (scope and type(scope[key]) ~= "function" and scope[key])
274 tpl.context.viewns = setmetatable({
276 include = function(name) tpl.Template(name):render(getfenv(2)) end;
277 translate = i18n.translate;
278 translatef = i18n.translatef;
279 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
280 striptags = util.striptags;
281 pcdata = util.pcdata;
283 theme = fs.basename(media);
284 resource = luci.config.main.resourcebase;
285 ifattr = function(...) return _ifattr(...) end;
286 attr = function(...) return _ifattr(true, ...) end;
287 }, {__index=function(table, key)
288 if key == "controller" then
290 elseif key == "REQUEST_URI" then
291 return build_url(unpack(ctx.requestpath))
293 return rawget(table, key) or _G[key]
298 track.dependent = (track.dependent ~= false)
299 assert(not track.dependent or not track.auto,
300 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
301 "has no parent node so the access to this location has been denied.\n" ..
302 "This is a software bug, please report this message at " ..
303 "http://luci.subsignal.org/trac/newticket"
306 if track.sysauth then
307 local authen = type(track.sysauth_authenticator) == "function"
308 and track.sysauth_authenticator
309 or authenticator[track.sysauth_authenticator]
311 local def = (type(track.sysauth) == "string") and track.sysauth
312 local accs = def and {track.sysauth} or track.sysauth
313 local sess = ctx.authsession
314 local verifytoken = false
316 sess = http.getcookie("sysauth")
317 sess = sess and sess:match("^[a-f0-9]*$")
321 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
325 if not verifytoken or ctx.urltoken.stok == sdat.token then
329 local eu = http.getenv("HTTP_AUTH_USER")
330 local ep = http.getenv("HTTP_AUTH_PASS")
331 if eu and ep and sys.user.checkpasswd(eu, ep) then
332 authen = function() return eu end
336 if not util.contains(accs, user) then
338 local user, sess = authen(sys.user.checkpasswd, accs, def)
340 if not user or not util.contains(accs, user) then
344 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
346 token = sys.uniqueid(16)
347 util.ubus("session", "set", {
348 ubus_rpc_session = sdat.ubus_rpc_session,
352 section = sys.uniqueid(16)
355 sess = sdat.ubus_rpc_session
359 if sess and token then
360 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
364 ctx.urltoken.stok = token
365 ctx.authsession = sess
368 http.redirect(build_url(unpack(ctx.requestpath)))
372 http.status(403, "Forbidden")
376 ctx.authsession = sess
381 if track.setgroup then
382 sys.process.setgroup(track.setgroup)
385 if track.setuser then
386 -- trigger ubus connection before dropping root privs
389 sys.process.setuser(track.setuser)
394 if type(c.target) == "function" then
396 elseif type(c.target) == "table" then
397 target = c.target.target
401 if c and (c.index or type(target) == "function") then
403 ctx.requested = ctx.requested or ctx.dispatched
406 if c and c.index then
407 local tpl = require "luci.template"
409 if util.copcall(tpl.render, "indexer", {}) then
414 if type(target) == "function" then
415 util.copcall(function()
416 local oldenv = getfenv(target)
417 local module = require(c.module)
418 local env = setmetatable({}, {__index=
421 return rawget(tbl, key) or module[key] or oldenv[key]
428 if type(c.target) == "table" then
429 ok, err = util.copcall(target, c.target, unpack(args))
431 ok, err = util.copcall(target, unpack(args))
434 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
435 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
436 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
439 if not root or not root.target then
440 error404("No root node was registered, this usually happens if no module was installed.\n" ..
441 "Install luci-mod-admin-full and retry. " ..
442 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
444 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
445 "If this url belongs to an extension, make sure it is properly installed.\n" ..
446 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
451 function createindex()
452 local controllers = { }
453 local base = "%s/controller/" % util.libpath()
456 for path in (fs.glob("%s*.lua" % base) or function() end) do
457 controllers[#controllers+1] = path
460 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
461 controllers[#controllers+1] = path
465 local cachedate = fs.stat(indexcache, "mtime")
468 for _, obj in ipairs(controllers) do
469 local omtime = fs.stat(obj, "mtime")
470 realdate = (omtime and omtime > realdate) and omtime or realdate
473 if cachedate > realdate and sys.process.info("uid") == 0 then
475 sys.process.info("uid") == fs.stat(indexcache, "uid")
476 and fs.stat(indexcache, "modestr") == "rw-------",
477 "Fatal: Indexcache is not sane!"
480 index = loadfile(indexcache)()
488 for _, path in ipairs(controllers) do
489 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
490 local mod = require(modname)
492 "Invalid controller file found\n" ..
493 "The file '" .. path .. "' contains an invalid module line.\n" ..
494 "Please verify whether the module name is set to '" .. modname ..
495 "' - It must correspond to the file path!")
497 local idx = mod.index
498 assert(type(idx) == "function",
499 "Invalid controller file found\n" ..
500 "The file '" .. path .. "' contains no index() function.\n" ..
501 "Please make sure that the controller contains a valid " ..
502 "index function and verify the spelling!")
508 local f = nixio.open(indexcache, "w", 600)
509 f:writeall(util.get_bytecode(index))
514 -- Build the index before if it does not exist yet.
515 function createtree()
521 local tree = {nodes={}, inreq=true}
524 ctx.treecache = setmetatable({}, {__mode="v"})
528 -- Load default translation
529 require "luci.i18n".loadc("base")
531 local scope = setmetatable({}, {__index = luci.dispatcher})
533 for k, v in pairs(index) do
539 local function modisort(a,b)
540 return modi[a].order < modi[b].order
543 for _, v in util.spairs(modi, modisort) do
544 scope._NAME = v.module
545 setfenv(v.func, scope)
552 function modifier(func, order)
553 context.modifiers[#context.modifiers+1] = {
561 function assign(path, clone, title, order)
562 local obj = node(unpack(path))
569 setmetatable(obj, {__index = _create_node(clone)})
574 function entry(path, target, title, order)
575 local c = node(unpack(path))
580 c.module = getfenv(2)._NAME
585 -- enabling the node.
587 return _create_node({...})
591 local c = _create_node({...})
593 c.module = getfenv(2)._NAME
599 function _create_node(path)
604 local name = table.concat(path, ".")
605 local c = context.treecache[name]
608 local last = table.remove(path)
609 local parent = _create_node(path)
611 c = {nodes={}, auto=true}
612 -- the node is "in request" if the request path matches
613 -- at least up to the length of the node path
614 if parent.inreq and context.path[#path+1] == last then
617 parent.nodes[last] = c
618 context.treecache[name] = c
625 function _firstchild()
626 local path = { unpack(context.path) }
627 local name = table.concat(path, ".")
628 local node = context.treecache[name]
631 if node and node.nodes and next(node.nodes) then
633 for k, v in pairs(node.nodes) do
635 (v.order or 100) < (node.nodes[lowest].order or 100)
642 assert(lowest ~= nil,
643 "The requested node contains no childs, unable to redispatch")
645 path[#path+1] = lowest
649 function firstchild()
650 return { type = "firstchild", target = _firstchild }
656 for _, r in ipairs({...}) do
664 function rewrite(n, ...)
667 local dispatched = util.clone(context.dispatched)
670 table.remove(dispatched, 1)
673 for i, r in ipairs(req) do
674 table.insert(dispatched, i, r)
677 for _, r in ipairs({...}) do
678 dispatched[#dispatched+1] = r
686 local function _call(self, ...)
687 local func = getfenv()[self.name]
689 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
691 assert(type(func) == "function",
692 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
693 'of type "' .. type(func) .. '".')
695 if #self.argv > 0 then
696 return func(unpack(self.argv), ...)
702 function call(name, ...)
703 return {type = "call", argv = {...}, name = name, target = _call}
707 local _template = function(self, ...)
708 require "luci.template".render(self.view)
711 function template(name)
712 return {type = "template", view = name, target = _template}
716 local function _cbi(self, ...)
717 local cbi = require "luci.cbi"
718 local tpl = require "luci.template"
719 local http = require "luci.http"
721 local config = self.config or {}
722 local maps = cbi.load(self.model, ...)
726 for i, res in ipairs(maps) do
728 local cstate = res:parse()
729 if cstate and (not state or cstate < state) then
734 local function _resolve_path(path)
735 return type(path) == "table" and build_url(unpack(path)) or path
738 if config.on_valid_to and state and state > 0 and state < 2 then
739 http.redirect(_resolve_path(config.on_valid_to))
743 if config.on_changed_to and state and state > 1 then
744 http.redirect(_resolve_path(config.on_changed_to))
748 if config.on_success_to and state and state > 0 then
749 http.redirect(_resolve_path(config.on_success_to))
753 if config.state_handler then
754 if not config.state_handler(state, maps) then
759 http.header("X-CBI-State", state or 0)
761 if not config.noheader then
762 tpl.render("cbi/header", {state = state})
767 local applymap = false
768 local pageaction = true
769 local parsechain = { }
771 for i, res in ipairs(maps) do
772 if res.apply_needed and res.parsechain then
774 for _, c in ipairs(res.parsechain) do
775 parsechain[#parsechain+1] = c
781 redirect = redirect or res.redirect
784 if res.pageaction == false then
789 messages = messages or { }
790 messages[#messages+1] = res.message
794 for i, res in ipairs(maps) do
800 pageaction = pageaction,
801 parsechain = parsechain
805 if not config.nofooter then
806 tpl.render("cbi/footer", {
808 pageaction = pageaction,
811 autoapply = config.autoapply
816 function cbi(model, config)
817 return {type = "cbi", config = config, model = model, target = _cbi}
821 local function _arcombine(self, ...)
823 local target = #argv > 0 and self.targets[2] or self.targets[1]
824 setfenv(target.target, self.env)
825 target:target(unpack(argv))
828 function arcombine(trg1, trg2)
829 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
833 local function _form(self, ...)
834 local cbi = require "luci.cbi"
835 local tpl = require "luci.template"
836 local http = require "luci.http"
838 local maps = luci.cbi.load(self.model, ...)
841 for i, res in ipairs(maps) do
842 local cstate = res:parse()
843 if cstate and (not state or cstate < state) then
848 http.header("X-CBI-State", state or 0)
850 for i, res in ipairs(maps) do
857 return {type = "cbi", model = model, target = _form}
860 translate = i18n.translate
862 -- This function does not actually translate the given argument but
863 -- is used by build/i18n-scan.pl to find translatable entries.