1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
4 --- LuCI web dispatcher.
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 --- Build the URL relative to the server webroot from given virtual path.
27 -- @param ... Virtual path
28 -- @return Relative URL
29 function build_url(...)
31 local url = { http.getenv("SCRIPT_NAME") or "" }
34 for k, v in pairs(context.urltoken) do
36 url[#url+1] = http.urlencode(k)
38 url[#url+1] = http.urlencode(v)
42 for _, p in ipairs(path) do
43 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
49 return table.concat(url, "")
52 --- Check whether a dispatch node shall be visible
53 -- @param node Dispatch node
54 -- @return Boolean indicating whether the node should be visible
55 function node_visible(node)
58 (not node.title or #node.title == 0) or
59 (not node.target or node.hidden == true) or
60 (type(node.target) == "table" and node.target.type == "firstchild" and
61 (type(node.nodes) ~= "table" or not next(node.nodes)))
67 --- Return a sorted table of visible childs within a given node
68 -- @param node Dispatch node
69 -- @return Ordered table of child node names
70 function node_childs(node)
74 for k, v in util.spairs(node.nodes,
76 return (node.nodes[a].order or 100)
77 < (node.nodes[b].order or 100)
80 if node_visible(v) then
89 --- Send a 404 error code and render the "error404" template if available.
90 -- @param message Custom error message (optional)
92 function error404(message)
93 http.status(404, "Not Found")
94 message = message or "Not Found"
96 require("luci.template")
97 if not util.copcall(luci.template.render, "error404") then
98 http.prepare_content("text/plain")
104 --- Send a 500 error code and render the "error500" template if available.
105 -- @param message Custom error message (optional)#
107 function error500(message)
109 if not context.template_header_sent then
110 http.status(500, "Internal Server Error")
111 http.prepare_content("text/plain")
114 require("luci.template")
115 if not util.copcall(luci.template.render, "error500", {message=message}) then
116 http.prepare_content("text/plain")
123 function authenticator.htmlauth(validator, accs, default)
124 local user = http.formvalue("luci_username")
125 local pass = http.formvalue("luci_password")
127 if user and validator(user, pass) then
132 require("luci.template")
134 luci.template.render("sysauth", {duser=default, fuser=user})
139 --- Dispatch an HTTP request.
140 -- @param request LuCI HTTP Request object
141 function httpdispatch(request, prefix)
142 http.context.request = request
146 context.urltoken = {}
148 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
151 for _, node in ipairs(prefix) do
156 local tokensok = true
157 for node in pathinfo:gmatch("[^/]+") do
160 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
163 context.urltoken[tkey] = tval
170 local stat, err = util.coxpcall(function()
171 dispatch(context.request)
176 --context._disable_memtrace()
179 --- Dispatches a LuCI virtual path.
180 -- @param request Virtual path
181 function dispatch(request)
182 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
186 local conf = require "luci.config"
188 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
190 local lang = conf.main.lang or "auto"
191 if lang == "auto" then
192 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
193 for lpat in aclang:gmatch("[%w-]+") do
194 lpat = lpat and lpat:gsub("-", "_")
195 if conf.languages[lpat] then
201 require "luci.i18n".setlanguage(lang)
212 ctx.requestargs = ctx.requestargs or args
214 local token = ctx.urltoken
218 for i, s in ipairs(request) do
227 util.update(track, c)
235 for j=n+1, #request do
236 args[#args+1] = request[j]
237 freq[#freq+1] = request[j]
241 ctx.requestpath = ctx.requestpath or freq
245 i18n.loadc(track.i18n)
248 -- Init template engine
249 if (c and c.index) or not track.notemplate then
250 local tpl = require("luci.template")
251 local media = track.mediaurlbase or luci.config.main.mediaurlbase
252 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
254 for name, theme in pairs(luci.config.themes) do
255 if name:sub(1,1) ~= "." and pcall(tpl.Template,
256 "themes/%s/header" % fs.basename(theme)) then
260 assert(media, "No valid theme found")
263 local function _ifattr(cond, key, val)
265 local env = getfenv(3)
266 local scope = (type(env.self) == "table") and env.self
267 return string.format(
268 ' %s="%s"', tostring(key),
269 util.pcdata(tostring( val
270 or (type(env[key]) ~= "function" and env[key])
271 or (scope and type(scope[key]) ~= "function" and scope[key])
279 tpl.context.viewns = setmetatable({
281 include = function(name) tpl.Template(name):render(getfenv(2)) end;
282 translate = i18n.translate;
283 translatef = i18n.translatef;
284 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
285 striptags = util.striptags;
286 pcdata = util.pcdata;
288 theme = fs.basename(media);
289 resource = luci.config.main.resourcebase;
290 ifattr = function(...) return _ifattr(...) end;
291 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))
298 return rawget(table, key) or _G[key]
303 track.dependent = (track.dependent ~= false)
304 assert(not track.dependent or not track.auto,
305 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
306 "has no parent node so the access to this location has been denied.\n" ..
307 "This is a software bug, please report this message at " ..
308 "http://luci.subsignal.org/trac/newticket"
311 if track.sysauth then
312 local authen = type(track.sysauth_authenticator) == "function"
313 and track.sysauth_authenticator
314 or authenticator[track.sysauth_authenticator]
316 local def = (type(track.sysauth) == "string") and track.sysauth
317 local accs = def and {track.sysauth} or track.sysauth
318 local sess = ctx.authsession
319 local verifytoken = false
321 sess = http.getcookie("sysauth")
322 sess = sess and sess:match("^[a-f0-9]*$")
326 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
330 if not verifytoken or ctx.urltoken.stok == sdat.token then
334 local eu = http.getenv("HTTP_AUTH_USER")
335 local ep = http.getenv("HTTP_AUTH_PASS")
336 if eu and ep and sys.user.checkpasswd(eu, ep) then
337 authen = function() return eu end
341 if not util.contains(accs, user) then
343 ctx.urltoken.stok = nil
344 local user, sess = authen(sys.user.checkpasswd, accs, def)
345 if not user or not util.contains(accs, user) then
349 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
351 local token = sys.uniqueid(16)
352 util.ubus("session", "set", {
353 ubus_rpc_session = sdat.ubus_rpc_session,
357 section = sys.uniqueid(16)
360 sess = sdat.ubus_rpc_session
361 ctx.urltoken.stok = token
366 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
367 ctx.authsession = sess
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 sys.process.setuser(track.setuser)
391 if type(c.target) == "function" then
393 elseif type(c.target) == "table" then
394 target = c.target.target
398 if c and (c.index or type(target) == "function") then
400 ctx.requested = ctx.requested or ctx.dispatched
403 if c and c.index then
404 local tpl = require "luci.template"
406 if util.copcall(tpl.render, "indexer", {}) then
411 if type(target) == "function" then
412 util.copcall(function()
413 local oldenv = getfenv(target)
414 local module = require(c.module)
415 local env = setmetatable({}, {__index=
418 return rawget(tbl, key) or module[key] or oldenv[key]
425 if type(c.target) == "table" then
426 ok, err = util.copcall(target, c.target, unpack(args))
428 ok, err = util.copcall(target, unpack(args))
431 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
432 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
433 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
436 if not root or not root.target then
437 error404("No root node was registered, this usually happens if no module was installed.\n" ..
438 "Install luci-mod-admin-full and retry. " ..
439 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
441 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
442 "If this url belongs to an extension, make sure it is properly installed.\n" ..
443 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
448 --- Generate the dispatching index using the native file-cache based strategy.
449 function createindex()
450 local controllers = { }
451 local base = "%s/controller/" % util.libpath()
454 for path in (fs.glob("%s*.lua" % base) or function() end) do
455 controllers[#controllers+1] = path
458 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
459 controllers[#controllers+1] = path
463 local cachedate = fs.stat(indexcache, "mtime")
466 for _, obj in ipairs(controllers) do
467 local omtime = fs.stat(obj, "mtime")
468 realdate = (omtime and omtime > realdate) and omtime or realdate
471 if cachedate > realdate and sys.process.info("uid") == 0 then
473 sys.process.info("uid") == fs.stat(indexcache, "uid")
474 and fs.stat(indexcache, "modestr") == "rw-------",
475 "Fatal: Indexcache is not sane!"
478 index = loadfile(indexcache)()
486 for _, path in ipairs(controllers) do
487 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
488 local mod = require(modname)
490 "Invalid controller file found\n" ..
491 "The file '" .. path .. "' contains an invalid module line.\n" ..
492 "Please verify whether the module name is set to '" .. modname ..
493 "' - It must correspond to the file path!")
495 local idx = mod.index
496 assert(type(idx) == "function",
497 "Invalid controller file found\n" ..
498 "The file '" .. path .. "' contains no index() function.\n" ..
499 "Please make sure that the controller contains a valid " ..
500 "index function and verify the spelling!")
506 local f = nixio.open(indexcache, "w", 600)
507 f:writeall(util.get_bytecode(index))
512 --- Create the dispatching tree from the index.
513 -- Build the index before if it does not exist yet.
514 function createtree()
520 local tree = {nodes={}, inreq=true}
523 ctx.treecache = setmetatable({}, {__mode="v"})
527 -- Load default translation
528 require "luci.i18n".loadc("base")
530 local scope = setmetatable({}, {__index = luci.dispatcher})
532 for k, v in pairs(index) do
538 local function modisort(a,b)
539 return modi[a].order < modi[b].order
542 for _, v in util.spairs(modi, modisort) do
543 scope._NAME = v.module
544 setfenv(v.func, scope)
551 --- Register a tree modifier.
552 -- @param func Modifier function
553 -- @param order Modifier order value (optional)
554 function modifier(func, order)
555 context.modifiers[#context.modifiers+1] = {
563 --- Clone a node of the dispatching tree to another position.
564 -- @param path Virtual path destination
565 -- @param clone Virtual path source
566 -- @param title Destination node title (optional)
567 -- @param order Destination node order value (optional)
568 -- @return Dispatching tree node
569 function assign(path, clone, title, order)
570 local obj = node(unpack(path))
577 setmetatable(obj, {__index = _create_node(clone)})
582 --- Create a new dispatching node and define common parameters.
583 -- @param path Virtual path
584 -- @param target Target function to call when dispatched.
585 -- @param title Destination node title
586 -- @param order Destination node order value (optional)
587 -- @return Dispatching tree node
588 function entry(path, target, title, order)
589 local c = node(unpack(path))
594 c.module = getfenv(2)._NAME
599 --- Fetch or create a dispatching node without setting the target module or
600 -- enabling the node.
601 -- @param ... Virtual path
602 -- @return Dispatching tree node
604 return _create_node({...})
607 --- Fetch or create a new dispatching node.
608 -- @param ... Virtual path
609 -- @return Dispatching tree node
611 local c = _create_node({...})
613 c.module = getfenv(2)._NAME
619 function _create_node(path)
624 local name = table.concat(path, ".")
625 local c = context.treecache[name]
628 local last = table.remove(path)
629 local parent = _create_node(path)
631 c = {nodes={}, auto=true}
632 -- the node is "in request" if the request path matches
633 -- at least up to the length of the node path
634 if parent.inreq and context.path[#path+1] == last then
637 parent.nodes[last] = c
638 context.treecache[name] = c
645 function _firstchild()
646 local path = { unpack(context.path) }
647 local name = table.concat(path, ".")
648 local node = context.treecache[name]
651 if node and node.nodes and next(node.nodes) then
653 for k, v in pairs(node.nodes) do
655 (v.order or 100) < (node.nodes[lowest].order or 100)
662 assert(lowest ~= nil,
663 "The requested node contains no childs, unable to redispatch")
665 path[#path+1] = lowest
669 --- Alias the first (lowest order) page automatically
670 function firstchild()
671 return { type = "firstchild", target = _firstchild }
674 --- Create a redirect to another dispatching node.
675 -- @param ... Virtual path destination
679 for _, r in ipairs({...}) do
687 --- Rewrite the first x path values of the request.
688 -- @param n Number of path values to replace
689 -- @param ... Virtual path to replace removed path values with
690 function rewrite(n, ...)
693 local dispatched = util.clone(context.dispatched)
696 table.remove(dispatched, 1)
699 for i, r in ipairs(req) do
700 table.insert(dispatched, i, r)
703 for _, r in ipairs({...}) do
704 dispatched[#dispatched+1] = r
712 local function _call(self, ...)
713 local func = getfenv()[self.name]
715 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
717 assert(type(func) == "function",
718 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
719 'of type "' .. type(func) .. '".')
721 if #self.argv > 0 then
722 return func(unpack(self.argv), ...)
728 --- Create a function-call dispatching target.
729 -- @param name Target function of local controller
730 -- @param ... Additional parameters passed to the function
731 function call(name, ...)
732 return {type = "call", argv = {...}, name = name, target = _call}
736 local _template = function(self, ...)
737 require "luci.template".render(self.view)
740 --- Create a template render dispatching target.
741 -- @param name Template to be rendered
742 function template(name)
743 return {type = "template", view = name, target = _template}
747 local function _cbi(self, ...)
748 local cbi = require "luci.cbi"
749 local tpl = require "luci.template"
750 local http = require "luci.http"
752 local config = self.config or {}
753 local maps = cbi.load(self.model, ...)
757 for i, res in ipairs(maps) do
759 local cstate = res:parse()
760 if cstate and (not state or cstate < state) then
765 local function _resolve_path(path)
766 return type(path) == "table" and build_url(unpack(path)) or path
769 if config.on_valid_to and state and state > 0 and state < 2 then
770 http.redirect(_resolve_path(config.on_valid_to))
774 if config.on_changed_to and state and state > 1 then
775 http.redirect(_resolve_path(config.on_changed_to))
779 if config.on_success_to and state and state > 0 then
780 http.redirect(_resolve_path(config.on_success_to))
784 if config.state_handler then
785 if not config.state_handler(state, maps) then
790 http.header("X-CBI-State", state or 0)
792 if not config.noheader then
793 tpl.render("cbi/header", {state = state})
798 local applymap = false
799 local pageaction = true
800 local parsechain = { }
802 for i, res in ipairs(maps) do
803 if res.apply_needed and res.parsechain then
805 for _, c in ipairs(res.parsechain) do
806 parsechain[#parsechain+1] = c
812 redirect = redirect or res.redirect
815 if res.pageaction == false then
820 messages = messages or { }
821 messages[#messages+1] = res.message
825 for i, res in ipairs(maps) do
831 pageaction = pageaction,
832 parsechain = parsechain
836 if not config.nofooter then
837 tpl.render("cbi/footer", {
839 pageaction = pageaction,
842 autoapply = config.autoapply
847 --- Create a CBI model dispatching target.
848 -- @param model CBI model to be rendered
849 function cbi(model, config)
850 return {type = "cbi", config = config, model = model, target = _cbi}
854 local function _arcombine(self, ...)
856 local target = #argv > 0 and self.targets[2] or self.targets[1]
857 setfenv(target.target, self.env)
858 target:target(unpack(argv))
861 --- Create a combined dispatching target for non argv and argv requests.
862 -- @param trg1 Overview Target
863 -- @param trg2 Detail Target
864 function arcombine(trg1, trg2)
865 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
869 local function _form(self, ...)
870 local cbi = require "luci.cbi"
871 local tpl = require "luci.template"
872 local http = require "luci.http"
874 local maps = luci.cbi.load(self.model, ...)
877 for i, res in ipairs(maps) do
878 local cstate = res:parse()
879 if cstate and (not state or cstate < state) then
884 http.header("X-CBI-State", state or 0)
886 for i, res in ipairs(maps) do
892 --- Create a CBI form model dispatching target.
893 -- @param model CBI form model tpo be rendered
895 return {type = "cbi", model = model, target = _form}
898 --- Access the luci.i18n translate() api.
901 -- @param text Text to translate
902 translate = i18n.translate
904 --- No-op function used to mark translation entries for menu labels.
905 -- This function does not actually translate the given argument but
906 -- is used by build/i18n-scan.pl to find translatable entries.