5 The request dispatcher and module dispatcher generators
11 Copyright 2008 Steven Barth <steven@midlink.org>
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
17 http://www.apache.org/licenses/LICENSE-2.0
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
27 --- LuCI web dispatcher.
28 local fs = require "nixio.fs"
29 local sys = require "luci.sys"
30 local util = require "luci.util"
31 local http = require "luci.http"
32 local nixio = require "nixio", require "nixio.util"
34 module("luci.dispatcher", package.seeall)
35 context = util.threadlocal()
36 uci = require "luci.model.uci"
37 i18n = require "luci.i18n"
49 --- Build the URL relative to the server webroot from given virtual path.
50 -- @param ... Virtual path
51 -- @return Relative URL
52 function build_url(...)
54 local url = { http.getenv("SCRIPT_NAME") or "" }
57 for k, v in pairs(context.urltoken) do
59 url[#url+1] = http.urlencode(k)
61 url[#url+1] = http.urlencode(v)
65 for _, p in ipairs(path) do
66 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
72 return table.concat(url, "")
75 --- Check whether a dispatch node shall be visible
76 -- @param node Dispatch node
77 -- @return Boolean indicating whether the node should be visible
78 function node_visible(node)
81 (not node.title or #node.title == 0) or
82 (not node.target or node.hidden == true) or
83 (type(node.target) == "table" and node.target.type == "firstchild" and
84 (type(node.nodes) ~= "table" or not next(node.nodes)))
90 --- Return a sorted table of visible childs within a given node
91 -- @param node Dispatch node
92 -- @return Ordered table of child node names
93 function node_childs(node)
97 for k, v in util.spairs(node.nodes,
99 return (node.nodes[a].order or 100)
100 < (node.nodes[b].order or 100)
103 if node_visible(v) then
112 --- Send a 404 error code and render the "error404" template if available.
113 -- @param message Custom error message (optional)
115 function error404(message)
116 http.status(404, "Not Found")
117 message = message or "Not Found"
119 require("luci.template")
120 if not util.copcall(luci.template.render, "error404") then
121 http.prepare_content("text/plain")
127 --- Send a 500 error code and render the "error500" template if available.
128 -- @param message Custom error message (optional)#
130 function error500(message)
132 if not context.template_header_sent then
133 http.status(500, "Internal Server Error")
134 http.prepare_content("text/plain")
137 require("luci.template")
138 if not util.copcall(luci.template.render, "error500", {message=message}) then
139 http.prepare_content("text/plain")
146 function authenticator.htmlauth(validator, accs, default)
147 local user = http.formvalue("luci_username")
148 local pass = http.formvalue("luci_password")
150 if user and validator(user, pass) then
155 require("luci.template")
157 luci.template.render("sysauth", {duser=default, fuser=user})
162 --- Dispatch an HTTP request.
163 -- @param request LuCI HTTP Request object
164 function httpdispatch(request, prefix)
165 http.context.request = request
169 context.urltoken = {}
171 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
174 for _, node in ipairs(prefix) do
179 local tokensok = true
180 for node in pathinfo:gmatch("[^/]+") do
183 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
186 context.urltoken[tkey] = tval
193 local stat, err = util.coxpcall(function()
194 dispatch(context.request)
199 --context._disable_memtrace()
202 --- Dispatches a LuCI virtual path.
203 -- @param request Virtual path
204 function dispatch(request)
205 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
209 local conf = require "luci.config"
211 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
213 local lang = conf.main.lang or "auto"
214 if lang == "auto" then
215 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
216 for lpat in aclang:gmatch("[%w-]+") do
217 lpat = lpat and lpat:gsub("-", "_")
218 if conf.languages[lpat] then
224 require "luci.i18n".setlanguage(lang)
235 ctx.requestargs = ctx.requestargs or args
237 local token = ctx.urltoken
241 for i, s in ipairs(request) do
250 util.update(track, c)
258 for j=n+1, #request do
259 args[#args+1] = request[j]
260 freq[#freq+1] = request[j]
264 ctx.requestpath = ctx.requestpath or freq
268 i18n.loadc(track.i18n)
271 -- Init template engine
272 if (c and c.index) or not track.notemplate then
273 local tpl = require("luci.template")
274 local media = track.mediaurlbase or luci.config.main.mediaurlbase
275 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
277 for name, theme in pairs(luci.config.themes) do
278 if name:sub(1,1) ~= "." and pcall(tpl.Template,
279 "themes/%s/header" % fs.basename(theme)) then
283 assert(media, "No valid theme found")
286 local function _ifattr(cond, key, val)
288 local env = getfenv(3)
289 local scope = (type(env.self) == "table") and env.self
290 return string.format(
291 ' %s="%s"', tostring(key),
292 util.pcdata(tostring( val
293 or (type(env[key]) ~= "function" and env[key])
294 or (scope and type(scope[key]) ~= "function" and scope[key])
302 tpl.context.viewns = setmetatable({
304 include = function(name) tpl.Template(name):render(getfenv(2)) end;
305 translate = i18n.translate;
306 translatef = i18n.translatef;
307 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
308 striptags = util.striptags;
309 pcdata = util.pcdata;
311 theme = fs.basename(media);
312 resource = luci.config.main.resourcebase;
313 ifattr = function(...) return _ifattr(...) end;
314 attr = function(...) return _ifattr(true, ...) end;
315 }, {__index=function(table, key)
316 if key == "controller" then
318 elseif key == "REQUEST_URI" then
319 return build_url(unpack(ctx.requestpath))
321 return rawget(table, key) or _G[key]
326 track.dependent = (track.dependent ~= false)
327 assert(not track.dependent or not track.auto,
328 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
329 "has no parent node so the access to this location has been denied.\n" ..
330 "This is a software bug, please report this message at " ..
331 "http://luci.subsignal.org/trac/newticket"
334 if track.sysauth then
335 local authen = type(track.sysauth_authenticator) == "function"
336 and track.sysauth_authenticator
337 or authenticator[track.sysauth_authenticator]
339 local def = (type(track.sysauth) == "string") and track.sysauth
340 local accs = def and {track.sysauth} or track.sysauth
341 local sess = ctx.authsession
342 local verifytoken = false
344 sess = http.getcookie("sysauth")
345 sess = sess and sess:match("^[a-f0-9]*$")
349 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
353 if not verifytoken or ctx.urltoken.stok == sdat.token then
357 local eu = http.getenv("HTTP_AUTH_USER")
358 local ep = http.getenv("HTTP_AUTH_PASS")
359 if eu and ep and sys.user.checkpasswd(eu, ep) then
360 authen = function() return eu end
364 if not util.contains(accs, user) then
366 ctx.urltoken.stok = nil
367 local user, sess = authen(sys.user.checkpasswd, accs, def)
368 if not user or not util.contains(accs, user) then
372 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
374 local token = sys.uniqueid(16)
375 util.ubus("session", "set", {
376 ubus_rpc_session = sdat.ubus_rpc_session,
380 section = sys.uniqueid(16)
383 sess = sdat.ubus_rpc_session
384 ctx.urltoken.stok = token
389 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
390 ctx.authsession = sess
395 http.status(403, "Forbidden")
399 ctx.authsession = sess
404 if track.setgroup then
405 sys.process.setgroup(track.setgroup)
408 if track.setuser then
409 sys.process.setuser(track.setuser)
414 if type(c.target) == "function" then
416 elseif type(c.target) == "table" then
417 target = c.target.target
421 if c and (c.index or type(target) == "function") then
423 ctx.requested = ctx.requested or ctx.dispatched
426 if c and c.index then
427 local tpl = require "luci.template"
429 if util.copcall(tpl.render, "indexer", {}) then
434 if type(target) == "function" then
435 util.copcall(function()
436 local oldenv = getfenv(target)
437 local module = require(c.module)
438 local env = setmetatable({}, {__index=
441 return rawget(tbl, key) or module[key] or oldenv[key]
448 if type(c.target) == "table" then
449 ok, err = util.copcall(target, c.target, unpack(args))
451 ok, err = util.copcall(target, unpack(args))
454 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
455 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
456 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
459 if not root or not root.target then
460 error404("No root node was registered, this usually happens if no module was installed.\n" ..
461 "Install luci-mod-admin-full and retry. " ..
462 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
464 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
465 "If this url belongs to an extension, make sure it is properly installed.\n" ..
466 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
471 --- Generate the dispatching index using the native file-cache based strategy.
472 function createindex()
473 local controllers = { }
474 local base = "%s/controller/" % util.libpath()
477 for path in (fs.glob("%s*.lua" % base) or function() end) do
478 controllers[#controllers+1] = path
481 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
482 controllers[#controllers+1] = path
486 local cachedate = fs.stat(indexcache, "mtime")
489 for _, obj in ipairs(controllers) do
490 local omtime = fs.stat(obj, "mtime")
491 realdate = (omtime and omtime > realdate) and omtime or realdate
494 if cachedate > realdate and sys.process.info("uid") == 0 then
496 sys.process.info("uid") == fs.stat(indexcache, "uid")
497 and fs.stat(indexcache, "modestr") == "rw-------",
498 "Fatal: Indexcache is not sane!"
501 index = loadfile(indexcache)()
509 for _, path in ipairs(controllers) do
510 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
511 local mod = require(modname)
513 "Invalid controller file found\n" ..
514 "The file '" .. path .. "' contains an invalid module line.\n" ..
515 "Please verify whether the module name is set to '" .. modname ..
516 "' - It must correspond to the file path!")
518 local idx = mod.index
519 assert(type(idx) == "function",
520 "Invalid controller file found\n" ..
521 "The file '" .. path .. "' contains no index() function.\n" ..
522 "Please make sure that the controller contains a valid " ..
523 "index function and verify the spelling!")
529 local f = nixio.open(indexcache, "w", 600)
530 f:writeall(util.get_bytecode(index))
535 --- Create the dispatching tree from the index.
536 -- Build the index before if it does not exist yet.
537 function createtree()
543 local tree = {nodes={}, inreq=true}
546 ctx.treecache = setmetatable({}, {__mode="v"})
550 -- Load default translation
551 require "luci.i18n".loadc("base")
553 local scope = setmetatable({}, {__index = luci.dispatcher})
555 for k, v in pairs(index) do
561 local function modisort(a,b)
562 return modi[a].order < modi[b].order
565 for _, v in util.spairs(modi, modisort) do
566 scope._NAME = v.module
567 setfenv(v.func, scope)
574 --- Register a tree modifier.
575 -- @param func Modifier function
576 -- @param order Modifier order value (optional)
577 function modifier(func, order)
578 context.modifiers[#context.modifiers+1] = {
586 --- Clone a node of the dispatching tree to another position.
587 -- @param path Virtual path destination
588 -- @param clone Virtual path source
589 -- @param title Destination node title (optional)
590 -- @param order Destination node order value (optional)
591 -- @return Dispatching tree node
592 function assign(path, clone, title, order)
593 local obj = node(unpack(path))
600 setmetatable(obj, {__index = _create_node(clone)})
605 --- Create a new dispatching node and define common parameters.
606 -- @param path Virtual path
607 -- @param target Target function to call when dispatched.
608 -- @param title Destination node title
609 -- @param order Destination node order value (optional)
610 -- @return Dispatching tree node
611 function entry(path, target, title, order)
612 local c = node(unpack(path))
617 c.module = getfenv(2)._NAME
622 --- Fetch or create a dispatching node without setting the target module or
623 -- enabling the node.
624 -- @param ... Virtual path
625 -- @return Dispatching tree node
627 return _create_node({...})
630 --- Fetch or create a new dispatching node.
631 -- @param ... Virtual path
632 -- @return Dispatching tree 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 --- Alias the first (lowest order) page automatically
693 function firstchild()
694 return { type = "firstchild", target = _firstchild }
697 --- Create a redirect to another dispatching node.
698 -- @param ... Virtual path destination
702 for _, r in ipairs({...}) do
710 --- Rewrite the first x path values of the request.
711 -- @param n Number of path values to replace
712 -- @param ... Virtual path to replace removed path values with
713 function rewrite(n, ...)
716 local dispatched = util.clone(context.dispatched)
719 table.remove(dispatched, 1)
722 for i, r in ipairs(req) do
723 table.insert(dispatched, i, r)
726 for _, r in ipairs({...}) do
727 dispatched[#dispatched+1] = r
735 local function _call(self, ...)
736 local func = getfenv()[self.name]
738 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
740 assert(type(func) == "function",
741 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
742 'of type "' .. type(func) .. '".')
744 if #self.argv > 0 then
745 return func(unpack(self.argv), ...)
751 --- Create a function-call dispatching target.
752 -- @param name Target function of local controller
753 -- @param ... Additional parameters passed to the function
754 function call(name, ...)
755 return {type = "call", argv = {...}, name = name, target = _call}
759 local _template = function(self, ...)
760 require "luci.template".render(self.view)
763 --- Create a template render dispatching target.
764 -- @param name Template to be rendered
765 function template(name)
766 return {type = "template", view = name, target = _template}
770 local function _cbi(self, ...)
771 local cbi = require "luci.cbi"
772 local tpl = require "luci.template"
773 local http = require "luci.http"
775 local config = self.config or {}
776 local maps = cbi.load(self.model, ...)
780 for i, res in ipairs(maps) do
782 local cstate = res:parse()
783 if cstate and (not state or cstate < state) then
788 local function _resolve_path(path)
789 return type(path) == "table" and build_url(unpack(path)) or path
792 if config.on_valid_to and state and state > 0 and state < 2 then
793 http.redirect(_resolve_path(config.on_valid_to))
797 if config.on_changed_to and state and state > 1 then
798 http.redirect(_resolve_path(config.on_changed_to))
802 if config.on_success_to and state and state > 0 then
803 http.redirect(_resolve_path(config.on_success_to))
807 if config.state_handler then
808 if not config.state_handler(state, maps) then
813 http.header("X-CBI-State", state or 0)
815 if not config.noheader then
816 tpl.render("cbi/header", {state = state})
821 local applymap = false
822 local pageaction = true
823 local parsechain = { }
825 for i, res in ipairs(maps) do
826 if res.apply_needed and res.parsechain then
828 for _, c in ipairs(res.parsechain) do
829 parsechain[#parsechain+1] = c
835 redirect = redirect or res.redirect
838 if res.pageaction == false then
843 messages = messages or { }
844 messages[#messages+1] = res.message
848 for i, res in ipairs(maps) do
854 pageaction = pageaction,
855 parsechain = parsechain
859 if not config.nofooter then
860 tpl.render("cbi/footer", {
862 pageaction = pageaction,
865 autoapply = config.autoapply
870 --- Create a CBI model dispatching target.
871 -- @param model CBI model to be rendered
872 function cbi(model, config)
873 return {type = "cbi", config = config, model = model, target = _cbi}
877 local function _arcombine(self, ...)
879 local target = #argv > 0 and self.targets[2] or self.targets[1]
880 setfenv(target.target, self.env)
881 target:target(unpack(argv))
884 --- Create a combined dispatching target for non argv and argv requests.
885 -- @param trg1 Overview Target
886 -- @param trg2 Detail Target
887 function arcombine(trg1, trg2)
888 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
892 local function _form(self, ...)
893 local cbi = require "luci.cbi"
894 local tpl = require "luci.template"
895 local http = require "luci.http"
897 local maps = luci.cbi.load(self.model, ...)
900 for i, res in ipairs(maps) do
901 local cstate = res:parse()
902 if cstate and (not state or cstate < state) then
907 http.header("X-CBI-State", state or 0)
909 for i, res in ipairs(maps) do
915 --- Create a CBI form model dispatching target.
916 -- @param model CBI form model tpo be rendered
918 return {type = "cbi", model = model, target = _form}
921 --- Access the luci.i18n translate() api.
924 -- @param text Text to translate
925 translate = i18n.translate
927 --- No-op function used to mark translation entries for menu labels.
928 -- This function does not actually translate the given argument but
929 -- is used by build/i18n-scan.pl to find translatable entries.