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
131 if context.urltoken.stok then
132 context.urltoken.stok = nil
133 http.header("Set-Cookie", "sysauth=; path="..build_url())
134 http.redirect(build_url())
137 require("luci.template")
139 http.status(403, "Forbidden")
140 luci.template.render("sysauth", {duser=default, fuser=user})
147 --- Dispatch an HTTP request.
148 -- @param request LuCI HTTP Request object
149 function httpdispatch(request, prefix)
150 http.context.request = request
154 context.urltoken = {}
156 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
159 for _, node in ipairs(prefix) do
164 local tokensok = true
165 for node in pathinfo:gmatch("[^/]+") do
168 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
171 context.urltoken[tkey] = tval
178 local stat, err = util.coxpcall(function()
179 dispatch(context.request)
184 --context._disable_memtrace()
187 --- Dispatches a LuCI virtual path.
188 -- @param request Virtual path
189 function dispatch(request)
190 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
194 local conf = require "luci.config"
196 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
198 local lang = conf.main.lang or "auto"
199 if lang == "auto" then
200 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
201 for lpat in aclang:gmatch("[%w-]+") do
202 lpat = lpat and lpat:gsub("-", "_")
203 if conf.languages[lpat] then
209 require "luci.i18n".setlanguage(lang)
220 ctx.requestargs = ctx.requestargs or args
222 local token = ctx.urltoken
226 for i, s in ipairs(request) do
235 util.update(track, c)
243 for j=n+1, #request do
244 args[#args+1] = request[j]
245 freq[#freq+1] = request[j]
249 ctx.requestpath = ctx.requestpath or freq
253 i18n.loadc(track.i18n)
256 -- Init template engine
257 if (c and c.index) or not track.notemplate then
258 local tpl = require("luci.template")
259 local media = track.mediaurlbase or luci.config.main.mediaurlbase
260 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
262 for name, theme in pairs(luci.config.themes) do
263 if name:sub(1,1) ~= "." and pcall(tpl.Template,
264 "themes/%s/header" % fs.basename(theme)) then
268 assert(media, "No valid theme found")
271 local function _ifattr(cond, key, val)
273 local env = getfenv(3)
274 local scope = (type(env.self) == "table") and env.self
275 return string.format(
276 ' %s="%s"', tostring(key),
277 util.pcdata(tostring( val
278 or (type(env[key]) ~= "function" and env[key])
279 or (scope and type(scope[key]) ~= "function" and scope[key])
287 tpl.context.viewns = setmetatable({
289 include = function(name) tpl.Template(name):render(getfenv(2)) end;
290 translate = i18n.translate;
291 translatef = i18n.translatef;
292 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
293 striptags = util.striptags;
294 pcdata = util.pcdata;
296 theme = fs.basename(media);
297 resource = luci.config.main.resourcebase;
298 ifattr = function(...) return _ifattr(...) end;
299 attr = function(...) return _ifattr(true, ...) end;
300 }, {__index=function(table, key)
301 if key == "controller" then
303 elseif key == "REQUEST_URI" then
304 return build_url(unpack(ctx.requestpath))
306 return rawget(table, key) or _G[key]
311 track.dependent = (track.dependent ~= false)
312 assert(not track.dependent or not track.auto,
313 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
314 "has no parent node so the access to this location has been denied.\n" ..
315 "This is a software bug, please report this message at " ..
316 "http://luci.subsignal.org/trac/newticket"
319 if track.sysauth then
320 local authen = type(track.sysauth_authenticator) == "function"
321 and track.sysauth_authenticator
322 or authenticator[track.sysauth_authenticator]
324 local def = (type(track.sysauth) == "string") and track.sysauth
325 local accs = def and {track.sysauth} or track.sysauth
326 local sess = ctx.authsession
327 local verifytoken = false
329 sess = http.getcookie("sysauth")
330 sess = sess and sess:match("^[a-f0-9]*$")
334 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
338 if not verifytoken or ctx.urltoken.stok == sdat.token then
342 local eu = http.getenv("HTTP_AUTH_USER")
343 local ep = http.getenv("HTTP_AUTH_PASS")
344 if eu and ep and sys.user.checkpasswd(eu, ep) then
345 authen = function() return eu end
349 if not util.contains(accs, user) then
351 local user, sess = authen(sys.user.checkpasswd, accs, def)
352 if not user or not util.contains(accs, user) then
356 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
358 local token = sys.uniqueid(16)
359 util.ubus("session", "set", {
360 ubus_rpc_session = sdat.ubus_rpc_session,
364 section = sys.uniqueid(16)
367 sess = sdat.ubus_rpc_session
368 ctx.urltoken.stok = token
373 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
374 http.redirect(build_url(unpack(ctx.requestpath)))
375 ctx.authsession = sess
380 http.status(403, "Forbidden")
384 ctx.authsession = sess
389 if track.setgroup then
390 sys.process.setgroup(track.setgroup)
393 if track.setuser then
394 sys.process.setuser(track.setuser)
399 if type(c.target) == "function" then
401 elseif type(c.target) == "table" then
402 target = c.target.target
406 if c and (c.index or type(target) == "function") then
408 ctx.requested = ctx.requested or ctx.dispatched
411 if c and c.index then
412 local tpl = require "luci.template"
414 if util.copcall(tpl.render, "indexer", {}) then
419 if type(target) == "function" then
420 util.copcall(function()
421 local oldenv = getfenv(target)
422 local module = require(c.module)
423 local env = setmetatable({}, {__index=
426 return rawget(tbl, key) or module[key] or oldenv[key]
433 if type(c.target) == "table" then
434 ok, err = util.copcall(target, c.target, unpack(args))
436 ok, err = util.copcall(target, unpack(args))
439 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
440 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
441 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
444 if not root or not root.target then
445 error404("No root node was registered, this usually happens if no module was installed.\n" ..
446 "Install luci-mod-admin-full and retry. " ..
447 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
449 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
450 "If this url belongs to an extension, make sure it is properly installed.\n" ..
451 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
456 --- Generate the dispatching index using the native file-cache based strategy.
457 function createindex()
458 local controllers = { }
459 local base = "%s/controller/" % util.libpath()
462 for path in (fs.glob("%s*.lua" % base) or function() end) do
463 controllers[#controllers+1] = path
466 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
467 controllers[#controllers+1] = path
471 local cachedate = fs.stat(indexcache, "mtime")
474 for _, obj in ipairs(controllers) do
475 local omtime = fs.stat(obj, "mtime")
476 realdate = (omtime and omtime > realdate) and omtime or realdate
479 if cachedate > realdate and sys.process.info("uid") == 0 then
481 sys.process.info("uid") == fs.stat(indexcache, "uid")
482 and fs.stat(indexcache, "modestr") == "rw-------",
483 "Fatal: Indexcache is not sane!"
486 index = loadfile(indexcache)()
494 for _, path in ipairs(controllers) do
495 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
496 local mod = require(modname)
498 "Invalid controller file found\n" ..
499 "The file '" .. path .. "' contains an invalid module line.\n" ..
500 "Please verify whether the module name is set to '" .. modname ..
501 "' - It must correspond to the file path!")
503 local idx = mod.index
504 assert(type(idx) == "function",
505 "Invalid controller file found\n" ..
506 "The file '" .. path .. "' contains no index() function.\n" ..
507 "Please make sure that the controller contains a valid " ..
508 "index function and verify the spelling!")
514 local f = nixio.open(indexcache, "w", 600)
515 f:writeall(util.get_bytecode(index))
520 --- Create the dispatching tree from the index.
521 -- Build the index before if it does not exist yet.
522 function createtree()
528 local tree = {nodes={}, inreq=true}
531 ctx.treecache = setmetatable({}, {__mode="v"})
535 -- Load default translation
536 require "luci.i18n".loadc("base")
538 local scope = setmetatable({}, {__index = luci.dispatcher})
540 for k, v in pairs(index) do
546 local function modisort(a,b)
547 return modi[a].order < modi[b].order
550 for _, v in util.spairs(modi, modisort) do
551 scope._NAME = v.module
552 setfenv(v.func, scope)
559 --- Register a tree modifier.
560 -- @param func Modifier function
561 -- @param order Modifier order value (optional)
562 function modifier(func, order)
563 context.modifiers[#context.modifiers+1] = {
571 --- Clone a node of the dispatching tree to another position.
572 -- @param path Virtual path destination
573 -- @param clone Virtual path source
574 -- @param title Destination node title (optional)
575 -- @param order Destination node order value (optional)
576 -- @return Dispatching tree node
577 function assign(path, clone, title, order)
578 local obj = node(unpack(path))
585 setmetatable(obj, {__index = _create_node(clone)})
590 --- Create a new dispatching node and define common parameters.
591 -- @param path Virtual path
592 -- @param target Target function to call when dispatched.
593 -- @param title Destination node title
594 -- @param order Destination node order value (optional)
595 -- @return Dispatching tree node
596 function entry(path, target, title, order)
597 local c = node(unpack(path))
602 c.module = getfenv(2)._NAME
607 --- Fetch or create a dispatching node without setting the target module or
608 -- enabling the node.
609 -- @param ... Virtual path
610 -- @return Dispatching tree node
612 return _create_node({...})
615 --- Fetch or create a new dispatching node.
616 -- @param ... Virtual path
617 -- @return Dispatching tree node
619 local c = _create_node({...})
621 c.module = getfenv(2)._NAME
627 function _create_node(path)
632 local name = table.concat(path, ".")
633 local c = context.treecache[name]
636 local last = table.remove(path)
637 local parent = _create_node(path)
639 c = {nodes={}, auto=true}
640 -- the node is "in request" if the request path matches
641 -- at least up to the length of the node path
642 if parent.inreq and context.path[#path+1] == last then
645 parent.nodes[last] = c
646 context.treecache[name] = c
653 function _firstchild()
654 local path = { unpack(context.path) }
655 local name = table.concat(path, ".")
656 local node = context.treecache[name]
659 if node and node.nodes and next(node.nodes) then
661 for k, v in pairs(node.nodes) do
663 (v.order or 100) < (node.nodes[lowest].order or 100)
670 assert(lowest ~= nil,
671 "The requested node contains no childs, unable to redispatch")
673 path[#path+1] = lowest
677 --- Alias the first (lowest order) page automatically
678 function firstchild()
679 return { type = "firstchild", target = _firstchild }
682 --- Create a redirect to another dispatching node.
683 -- @param ... Virtual path destination
687 for _, r in ipairs({...}) do
695 --- Rewrite the first x path values of the request.
696 -- @param n Number of path values to replace
697 -- @param ... Virtual path to replace removed path values with
698 function rewrite(n, ...)
701 local dispatched = util.clone(context.dispatched)
704 table.remove(dispatched, 1)
707 for i, r in ipairs(req) do
708 table.insert(dispatched, i, r)
711 for _, r in ipairs({...}) do
712 dispatched[#dispatched+1] = r
720 local function _call(self, ...)
721 local func = getfenv()[self.name]
723 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
725 assert(type(func) == "function",
726 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
727 'of type "' .. type(func) .. '".')
729 if #self.argv > 0 then
730 return func(unpack(self.argv), ...)
736 --- Create a function-call dispatching target.
737 -- @param name Target function of local controller
738 -- @param ... Additional parameters passed to the function
739 function call(name, ...)
740 return {type = "call", argv = {...}, name = name, target = _call}
744 local _template = function(self, ...)
745 require "luci.template".render(self.view)
748 --- Create a template render dispatching target.
749 -- @param name Template to be rendered
750 function template(name)
751 return {type = "template", view = name, target = _template}
755 local function _cbi(self, ...)
756 local cbi = require "luci.cbi"
757 local tpl = require "luci.template"
758 local http = require "luci.http"
760 local config = self.config or {}
761 local maps = cbi.load(self.model, ...)
765 for i, res in ipairs(maps) do
767 local cstate = res:parse()
768 if cstate and (not state or cstate < state) then
773 local function _resolve_path(path)
774 return type(path) == "table" and build_url(unpack(path)) or path
777 if config.on_valid_to and state and state > 0 and state < 2 then
778 http.redirect(_resolve_path(config.on_valid_to))
782 if config.on_changed_to and state and state > 1 then
783 http.redirect(_resolve_path(config.on_changed_to))
787 if config.on_success_to and state and state > 0 then
788 http.redirect(_resolve_path(config.on_success_to))
792 if config.state_handler then
793 if not config.state_handler(state, maps) then
798 http.header("X-CBI-State", state or 0)
800 if not config.noheader then
801 tpl.render("cbi/header", {state = state})
806 local applymap = false
807 local pageaction = true
808 local parsechain = { }
810 for i, res in ipairs(maps) do
811 if res.apply_needed and res.parsechain then
813 for _, c in ipairs(res.parsechain) do
814 parsechain[#parsechain+1] = c
820 redirect = redirect or res.redirect
823 if res.pageaction == false then
828 messages = messages or { }
829 messages[#messages+1] = res.message
833 for i, res in ipairs(maps) do
839 pageaction = pageaction,
840 parsechain = parsechain
844 if not config.nofooter then
845 tpl.render("cbi/footer", {
847 pageaction = pageaction,
850 autoapply = config.autoapply
855 --- Create a CBI model dispatching target.
856 -- @param model CBI model to be rendered
857 function cbi(model, config)
858 return {type = "cbi", config = config, model = model, target = _cbi}
862 local function _arcombine(self, ...)
864 local target = #argv > 0 and self.targets[2] or self.targets[1]
865 setfenv(target.target, self.env)
866 target:target(unpack(argv))
869 --- Create a combined dispatching target for non argv and argv requests.
870 -- @param trg1 Overview Target
871 -- @param trg2 Detail Target
872 function arcombine(trg1, trg2)
873 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
877 local function _form(self, ...)
878 local cbi = require "luci.cbi"
879 local tpl = require "luci.template"
880 local http = require "luci.http"
882 local maps = luci.cbi.load(self.model, ...)
885 for i, res in ipairs(maps) do
886 local cstate = res:parse()
887 if cstate and (not state or cstate < state) then
892 http.header("X-CBI-State", state or 0)
894 for i, res in ipairs(maps) do
900 --- Create a CBI form model dispatching target.
901 -- @param model CBI form model tpo be rendered
903 return {type = "cbi", model = model, target = _form}
906 --- Access the luci.i18n translate() api.
909 -- @param text Text to translate
910 translate = i18n.translate
912 --- No-op function used to mark translation entries for menu labels.
913 -- This function does not actually translate the given argument but
914 -- is used by build/i18n-scan.pl to find translatable entries.