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
117 http.header("Set-Cookie", "sysauth=; path="..build_url())
118 http.redirect(build_url())
121 require("luci.template")
123 http.status(403, "Forbidden")
124 luci.template.render("sysauth", {duser=default, fuser=user})
131 function httpdispatch(request, prefix)
132 http.context.request = request
136 context.urltoken = {}
138 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
141 for _, node in ipairs(prefix) do
146 local tokensok = true
147 for node in pathinfo:gmatch("[^/]+") do
150 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
153 context.urltoken[tkey] = tval
160 local stat, err = util.coxpcall(function()
161 dispatch(context.request)
166 --context._disable_memtrace()
169 function dispatch(request)
170 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
174 local conf = require "luci.config"
176 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
178 local lang = conf.main.lang or "auto"
179 if lang == "auto" then
180 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
181 for lpat in aclang:gmatch("[%w-]+") do
182 lpat = lpat and lpat:gsub("-", "_")
183 if conf.languages[lpat] then
189 require "luci.i18n".setlanguage(lang)
200 ctx.requestargs = ctx.requestargs or args
202 local token = ctx.urltoken
206 for i, s in ipairs(request) do
215 util.update(track, c)
223 for j=n+1, #request do
224 args[#args+1] = request[j]
225 freq[#freq+1] = request[j]
229 ctx.requestpath = ctx.requestpath or freq
233 i18n.loadc(track.i18n)
236 -- Init template engine
237 if (c and c.index) or not track.notemplate then
238 local tpl = require("luci.template")
239 local media = track.mediaurlbase or luci.config.main.mediaurlbase
240 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
242 for name, theme in pairs(luci.config.themes) do
243 if name:sub(1,1) ~= "." and pcall(tpl.Template,
244 "themes/%s/header" % fs.basename(theme)) then
248 assert(media, "No valid theme found")
251 local function _ifattr(cond, key, val)
253 local env = getfenv(3)
254 local scope = (type(env.self) == "table") and env.self
255 return string.format(
256 ' %s="%s"', tostring(key),
257 util.pcdata(tostring( val
258 or (type(env[key]) ~= "function" and env[key])
259 or (scope and type(scope[key]) ~= "function" and scope[key])
267 tpl.context.viewns = setmetatable({
269 include = function(name) tpl.Template(name):render(getfenv(2)) end;
270 translate = i18n.translate;
271 translatef = i18n.translatef;
272 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
273 striptags = util.striptags;
274 pcdata = util.pcdata;
276 theme = fs.basename(media);
277 resource = luci.config.main.resourcebase;
278 ifattr = function(...) return _ifattr(...) end;
279 attr = function(...) return _ifattr(true, ...) end;
280 }, {__index=function(table, key)
281 if key == "controller" then
283 elseif key == "REQUEST_URI" then
284 return build_url(unpack(ctx.requestpath))
286 return rawget(table, key) or _G[key]
291 track.dependent = (track.dependent ~= false)
292 assert(not track.dependent or not track.auto,
293 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
294 "has no parent node so the access to this location has been denied.\n" ..
295 "This is a software bug, please report this message at " ..
296 "http://luci.subsignal.org/trac/newticket"
299 if track.sysauth then
300 local authen = type(track.sysauth_authenticator) == "function"
301 and track.sysauth_authenticator
302 or authenticator[track.sysauth_authenticator]
304 local def = (type(track.sysauth) == "string") and track.sysauth
305 local accs = def and {track.sysauth} or track.sysauth
306 local sess = ctx.authsession
307 local verifytoken = false
309 sess = http.getcookie("sysauth")
310 sess = sess and sess:match("^[a-f0-9]*$")
314 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
318 if not verifytoken or ctx.urltoken.stok == sdat.token then
322 local eu = http.getenv("HTTP_AUTH_USER")
323 local ep = http.getenv("HTTP_AUTH_PASS")
324 if eu and ep and sys.user.checkpasswd(eu, ep) then
325 authen = function() return eu end
329 if not util.contains(accs, user) then
331 local user, sess = authen(sys.user.checkpasswd, accs, def)
332 if not user or not util.contains(accs, user) then
336 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
338 local token = sys.uniqueid(16)
339 util.ubus("session", "set", {
340 ubus_rpc_session = sdat.ubus_rpc_session,
344 section = sys.uniqueid(16)
347 sess = sdat.ubus_rpc_session
348 ctx.urltoken.stok = token
353 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
354 http.redirect(build_url(unpack(ctx.requestpath)))
355 ctx.authsession = sess
360 http.status(403, "Forbidden")
364 ctx.authsession = sess
369 if track.setgroup then
370 sys.process.setgroup(track.setgroup)
373 if track.setuser then
374 -- trigger ubus connection before dropping root privs
377 sys.process.setuser(track.setuser)
382 if type(c.target) == "function" then
384 elseif type(c.target) == "table" then
385 target = c.target.target
389 if c and (c.index or type(target) == "function") then
391 ctx.requested = ctx.requested or ctx.dispatched
394 if c and c.index then
395 local tpl = require "luci.template"
397 if util.copcall(tpl.render, "indexer", {}) then
402 if type(target) == "function" then
403 util.copcall(function()
404 local oldenv = getfenv(target)
405 local module = require(c.module)
406 local env = setmetatable({}, {__index=
409 return rawget(tbl, key) or module[key] or oldenv[key]
416 if type(c.target) == "table" then
417 ok, err = util.copcall(target, c.target, unpack(args))
419 ok, err = util.copcall(target, unpack(args))
422 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
423 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
424 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
427 if not root or not root.target then
428 error404("No root node was registered, this usually happens if no module was installed.\n" ..
429 "Install luci-mod-admin-full and retry. " ..
430 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
432 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
433 "If this url belongs to an extension, make sure it is properly installed.\n" ..
434 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
439 function createindex()
440 local controllers = { }
441 local base = "%s/controller/" % util.libpath()
444 for path in (fs.glob("%s*.lua" % base) or function() end) do
445 controllers[#controllers+1] = path
448 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
449 controllers[#controllers+1] = path
453 local cachedate = fs.stat(indexcache, "mtime")
456 for _, obj in ipairs(controllers) do
457 local omtime = fs.stat(obj, "mtime")
458 realdate = (omtime and omtime > realdate) and omtime or realdate
461 if cachedate > realdate and sys.process.info("uid") == 0 then
463 sys.process.info("uid") == fs.stat(indexcache, "uid")
464 and fs.stat(indexcache, "modestr") == "rw-------",
465 "Fatal: Indexcache is not sane!"
468 index = loadfile(indexcache)()
476 for _, path in ipairs(controllers) do
477 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
478 local mod = require(modname)
480 "Invalid controller file found\n" ..
481 "The file '" .. path .. "' contains an invalid module line.\n" ..
482 "Please verify whether the module name is set to '" .. modname ..
483 "' - It must correspond to the file path!")
485 local idx = mod.index
486 assert(type(idx) == "function",
487 "Invalid controller file found\n" ..
488 "The file '" .. path .. "' contains no index() function.\n" ..
489 "Please make sure that the controller contains a valid " ..
490 "index function and verify the spelling!")
496 local f = nixio.open(indexcache, "w", 600)
497 f:writeall(util.get_bytecode(index))
502 -- Build the index before if it does not exist yet.
503 function createtree()
509 local tree = {nodes={}, inreq=true}
512 ctx.treecache = setmetatable({}, {__mode="v"})
516 -- Load default translation
517 require "luci.i18n".loadc("base")
519 local scope = setmetatable({}, {__index = luci.dispatcher})
521 for k, v in pairs(index) do
527 local function modisort(a,b)
528 return modi[a].order < modi[b].order
531 for _, v in util.spairs(modi, modisort) do
532 scope._NAME = v.module
533 setfenv(v.func, scope)
540 function modifier(func, order)
541 context.modifiers[#context.modifiers+1] = {
549 function assign(path, clone, title, order)
550 local obj = node(unpack(path))
557 setmetatable(obj, {__index = _create_node(clone)})
562 function entry(path, target, title, order)
563 local c = node(unpack(path))
568 c.module = getfenv(2)._NAME
573 -- enabling the node.
575 return _create_node({...})
579 local c = _create_node({...})
581 c.module = getfenv(2)._NAME
587 function _create_node(path)
592 local name = table.concat(path, ".")
593 local c = context.treecache[name]
596 local last = table.remove(path)
597 local parent = _create_node(path)
599 c = {nodes={}, auto=true}
600 -- the node is "in request" if the request path matches
601 -- at least up to the length of the node path
602 if parent.inreq and context.path[#path+1] == last then
605 parent.nodes[last] = c
606 context.treecache[name] = c
613 function _firstchild()
614 local path = { unpack(context.path) }
615 local name = table.concat(path, ".")
616 local node = context.treecache[name]
619 if node and node.nodes and next(node.nodes) then
621 for k, v in pairs(node.nodes) do
623 (v.order or 100) < (node.nodes[lowest].order or 100)
630 assert(lowest ~= nil,
631 "The requested node contains no childs, unable to redispatch")
633 path[#path+1] = lowest
637 function firstchild()
638 return { type = "firstchild", target = _firstchild }
644 for _, r in ipairs({...}) do
652 function rewrite(n, ...)
655 local dispatched = util.clone(context.dispatched)
658 table.remove(dispatched, 1)
661 for i, r in ipairs(req) do
662 table.insert(dispatched, i, r)
665 for _, r in ipairs({...}) do
666 dispatched[#dispatched+1] = r
674 local function _call(self, ...)
675 local func = getfenv()[self.name]
677 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
679 assert(type(func) == "function",
680 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
681 'of type "' .. type(func) .. '".')
683 if #self.argv > 0 then
684 return func(unpack(self.argv), ...)
690 function call(name, ...)
691 return {type = "call", argv = {...}, name = name, target = _call}
695 local _template = function(self, ...)
696 require "luci.template".render(self.view)
699 function template(name)
700 return {type = "template", view = name, target = _template}
704 local function _cbi(self, ...)
705 local cbi = require "luci.cbi"
706 local tpl = require "luci.template"
707 local http = require "luci.http"
709 local config = self.config or {}
710 local maps = cbi.load(self.model, ...)
714 for i, res in ipairs(maps) do
716 local cstate = res:parse()
717 if cstate and (not state or cstate < state) then
722 local function _resolve_path(path)
723 return type(path) == "table" and build_url(unpack(path)) or path
726 if config.on_valid_to and state and state > 0 and state < 2 then
727 http.redirect(_resolve_path(config.on_valid_to))
731 if config.on_changed_to and state and state > 1 then
732 http.redirect(_resolve_path(config.on_changed_to))
736 if config.on_success_to and state and state > 0 then
737 http.redirect(_resolve_path(config.on_success_to))
741 if config.state_handler then
742 if not config.state_handler(state, maps) then
747 http.header("X-CBI-State", state or 0)
749 if not config.noheader then
750 tpl.render("cbi/header", {state = state})
755 local applymap = false
756 local pageaction = true
757 local parsechain = { }
759 for i, res in ipairs(maps) do
760 if res.apply_needed and res.parsechain then
762 for _, c in ipairs(res.parsechain) do
763 parsechain[#parsechain+1] = c
769 redirect = redirect or res.redirect
772 if res.pageaction == false then
777 messages = messages or { }
778 messages[#messages+1] = res.message
782 for i, res in ipairs(maps) do
788 pageaction = pageaction,
789 parsechain = parsechain
793 if not config.nofooter then
794 tpl.render("cbi/footer", {
796 pageaction = pageaction,
799 autoapply = config.autoapply
804 function cbi(model, config)
805 return {type = "cbi", config = config, model = model, target = _cbi}
809 local function _arcombine(self, ...)
811 local target = #argv > 0 and self.targets[2] or self.targets[1]
812 setfenv(target.target, self.env)
813 target:target(unpack(argv))
816 function arcombine(trg1, trg2)
817 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
821 local function _form(self, ...)
822 local cbi = require "luci.cbi"
823 local tpl = require "luci.template"
824 local http = require "luci.http"
826 local maps = luci.cbi.load(self.model, ...)
829 for i, res in ipairs(maps) do
830 local cstate = res:parse()
831 if cstate and (not state or cstate < state) then
836 http.header("X-CBI-State", state or 0)
838 for i, res in ipairs(maps) do
845 return {type = "cbi", model = model, target = _form}
848 translate = i18n.translate
850 -- This function does not actually translate the given argument but
851 -- is used by build/i18n-scan.pl to find translatable entries.