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 init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37 uci = require "luci.model.uci"
38 i18n = require "luci.i18n"
50 --- Build the URL relative to the server webroot from given virtual path.
51 -- @param ... Virtual path
52 -- @return Relative URL
53 function build_url(...)
55 local url = { http.getenv("SCRIPT_NAME") or "" }
58 for k, v in pairs(context.urltoken) do
60 url[#url+1] = http.urlencode(k)
62 url[#url+1] = http.urlencode(v)
66 for _, p in ipairs(path) do
67 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
73 return table.concat(url, "")
76 --- Check whether a dispatch node shall be visible
77 -- @param node Dispatch node
78 -- @return Boolean indicating whether the node should be visible
79 function node_visible(node)
82 (not node.title or #node.title == 0) or
83 (not node.target or node.hidden == true) or
84 (type(node.target) == "table" and node.target.type == "firstchild" and
85 (type(node.nodes) ~= "table" or not next(node.nodes)))
91 --- Return a sorted table of visible childs within a given node
92 -- @param node Dispatch node
93 -- @return Ordered table of child node names
94 function node_childs(node)
98 for k, v in util.spairs(node.nodes,
100 return (node.nodes[a].order or 100)
101 < (node.nodes[b].order or 100)
104 if node_visible(v) then
113 --- Send a 404 error code and render the "error404" template if available.
114 -- @param message Custom error message (optional)
116 function error404(message)
117 luci.http.status(404, "Not Found")
118 message = message or "Not Found"
120 require("luci.template")
121 if not luci.util.copcall(luci.template.render, "error404") then
122 luci.http.prepare_content("text/plain")
123 luci.http.write(message)
128 --- Send a 500 error code and render the "error500" template if available.
129 -- @param message Custom error message (optional)#
131 function error500(message)
132 luci.util.perror(message)
133 if not context.template_header_sent then
134 luci.http.status(500, "Internal Server Error")
135 luci.http.prepare_content("text/plain")
136 luci.http.write(message)
138 require("luci.template")
139 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
140 luci.http.prepare_content("text/plain")
141 luci.http.write(message)
147 function authenticator.htmlauth(validator, accs, default)
148 local user = luci.http.formvalue("luci_username")
149 local pass = luci.http.formvalue("luci_password")
151 if user and validator(user, pass) then
156 require("luci.template")
158 luci.template.render("sysauth", {duser=default, fuser=user})
163 --- Dispatch an HTTP request.
164 -- @param request LuCI HTTP Request object
165 function httpdispatch(request, prefix)
166 luci.http.context.request = request
170 context.urltoken = {}
172 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
175 for _, node in ipairs(prefix) do
180 local tokensok = true
181 for node in pathinfo:gmatch("[^/]+") do
184 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
187 context.urltoken[tkey] = tval
194 local stat, err = util.coxpcall(function()
195 dispatch(context.request)
200 --context._disable_memtrace()
203 --- Dispatches a LuCI virtual path.
204 -- @param request Virtual path
205 function dispatch(request)
206 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
210 local conf = require "luci.config"
212 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
214 local lang = conf.main.lang or "auto"
215 if lang == "auto" then
216 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
217 for lpat in aclang:gmatch("[%w-]+") do
218 lpat = lpat and lpat:gsub("-", "_")
219 if conf.languages[lpat] then
225 require "luci.i18n".setlanguage(lang)
236 ctx.requestargs = ctx.requestargs or args
238 local token = ctx.urltoken
242 for i, s in ipairs(request) do
251 util.update(track, c)
259 for j=n+1, #request do
260 args[#args+1] = request[j]
261 freq[#freq+1] = request[j]
265 ctx.requestpath = ctx.requestpath or freq
269 i18n.loadc(track.i18n)
272 -- Init template engine
273 if (c and c.index) or not track.notemplate then
274 local tpl = require("luci.template")
275 local media = track.mediaurlbase or luci.config.main.mediaurlbase
276 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
278 for name, theme in pairs(luci.config.themes) do
279 if name:sub(1,1) ~= "." and pcall(tpl.Template,
280 "themes/%s/header" % fs.basename(theme)) then
284 assert(media, "No valid theme found")
287 local function _ifattr(cond, key, val)
289 local env = getfenv(3)
290 local scope = (type(env.self) == "table") and env.self
291 return string.format(
292 ' %s="%s"', tostring(key),
293 luci.util.pcdata(tostring( val
294 or (type(env[key]) ~= "function" and env[key])
295 or (scope and type(scope[key]) ~= "function" and scope[key])
303 tpl.context.viewns = setmetatable({
304 write = luci.http.write;
305 include = function(name) tpl.Template(name):render(getfenv(2)) end;
306 translate = i18n.translate;
307 translatef = i18n.translatef;
308 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
309 striptags = util.striptags;
310 pcdata = util.pcdata;
312 theme = fs.basename(media);
313 resource = luci.config.main.resourcebase;
314 ifattr = function(...) return _ifattr(...) end;
315 attr = function(...) return _ifattr(true, ...) end;
316 }, {__index=function(table, key)
317 if key == "controller" then
319 elseif key == "REQUEST_URI" then
320 return build_url(unpack(ctx.requestpath))
322 return rawget(table, key) or _G[key]
327 track.dependent = (track.dependent ~= false)
328 assert(not track.dependent or not track.auto,
329 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
330 "has no parent node so the access to this location has been denied.\n" ..
331 "This is a software bug, please report this message at " ..
332 "http://luci.subsignal.org/trac/newticket"
335 if track.sysauth then
336 local authen = type(track.sysauth_authenticator) == "function"
337 and track.sysauth_authenticator
338 or authenticator[track.sysauth_authenticator]
340 local def = (type(track.sysauth) == "string") and track.sysauth
341 local accs = def and {track.sysauth} or track.sysauth
342 local sess = ctx.authsession
343 local verifytoken = false
345 sess = luci.http.getcookie("sysauth")
346 sess = sess and sess:match("^[a-f0-9]*$")
350 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
354 if not verifytoken or ctx.urltoken.stok == sdat.token then
358 local eu = http.getenv("HTTP_AUTH_USER")
359 local ep = http.getenv("HTTP_AUTH_PASS")
360 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
361 authen = function() return eu end
365 if not util.contains(accs, user) then
367 ctx.urltoken.stok = nil
368 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
369 if not user or not util.contains(accs, user) then
373 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
375 local token = luci.sys.uniqueid(16)
376 util.ubus("session", "set", {
377 ubus_rpc_session = sdat.ubus_rpc_session,
381 section = luci.sys.uniqueid(16)
384 sess = sdat.ubus_rpc_session
385 ctx.urltoken.stok = token
390 luci.http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
391 ctx.authsession = sess
396 luci.http.status(403, "Forbidden")
400 ctx.authsession = sess
405 if track.setgroup then
406 luci.sys.process.setgroup(track.setgroup)
409 if track.setuser then
410 luci.sys.process.setuser(track.setuser)
415 if type(c.target) == "function" then
417 elseif type(c.target) == "table" then
418 target = c.target.target
422 if c and (c.index or type(target) == "function") then
424 ctx.requested = ctx.requested or ctx.dispatched
427 if c and c.index then
428 local tpl = require "luci.template"
430 if util.copcall(tpl.render, "indexer", {}) then
435 if type(target) == "function" then
436 util.copcall(function()
437 local oldenv = getfenv(target)
438 local module = require(c.module)
439 local env = setmetatable({}, {__index=
442 return rawget(tbl, key) or module[key] or oldenv[key]
449 if type(c.target) == "table" then
450 ok, err = util.copcall(target, c.target, unpack(args))
452 ok, err = util.copcall(target, unpack(args))
455 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
456 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
457 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
460 if not root or not root.target then
461 error404("No root node was registered, this usually happens if no module was installed.\n" ..
462 "Install luci-mod-admin-full and retry. " ..
463 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
465 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
466 "If this url belongs to an extension, make sure it is properly installed.\n" ..
467 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
472 --- Generate the dispatching index using the best possible strategy.
473 function createindex()
474 local path = luci.util.libpath() .. "/controller/"
475 local suff = { ".lua", ".lua.gz" }
477 if luci.util.copcall(require, "luci.fastindex") then
478 createindex_fastindex(path, suff)
480 createindex_plain(path, suff)
484 --- Generate the dispatching index using the fastindex C-indexer.
485 -- @param path Controller base directory
486 -- @param suffixes Controller file suffixes
487 function createindex_fastindex(path, suffixes)
491 fi = luci.fastindex.new("index")
492 for _, suffix in ipairs(suffixes) do
493 fi.add(path .. "*" .. suffix)
494 fi.add(path .. "*/*" .. suffix)
499 for k, v in pairs(fi.indexes) do
504 --- Generate the dispatching index using the native file-cache based strategy.
505 -- @param path Controller base directory
506 -- @param suffixes Controller file suffixes
507 function createindex_plain(path, suffixes)
508 local controllers = { }
509 for _, suffix in ipairs(suffixes) do
510 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
511 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
515 local cachedate = fs.stat(indexcache, "mtime")
518 for _, obj in ipairs(controllers) do
519 local omtime = fs.stat(obj, "mtime")
520 realdate = (omtime and omtime > realdate) and omtime or realdate
523 if cachedate > realdate then
525 sys.process.info("uid") == fs.stat(indexcache, "uid")
526 and fs.stat(indexcache, "modestr") == "rw-------",
527 "Fatal: Indexcache is not sane!"
530 index = loadfile(indexcache)()
538 for i,c in ipairs(controllers) do
539 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
540 for _, suffix in ipairs(suffixes) do
541 modname = modname:gsub(suffix.."$", "")
544 local mod = require(modname)
546 "Invalid controller file found\n" ..
547 "The file '" .. c .. "' contains an invalid module line.\n" ..
548 "Please verify whether the module name is set to '" .. modname ..
549 "' - It must correspond to the file path!")
551 local idx = mod.index
552 assert(type(idx) == "function",
553 "Invalid controller file found\n" ..
554 "The file '" .. c .. "' contains no index() function.\n" ..
555 "Please make sure that the controller contains a valid " ..
556 "index function and verify the spelling!")
562 local f = nixio.open(indexcache, "w", 600)
563 f:writeall(util.get_bytecode(index))
568 --- Create the dispatching tree from the index.
569 -- Build the index before if it does not exist yet.
570 function createtree()
576 local tree = {nodes={}, inreq=true}
579 ctx.treecache = setmetatable({}, {__mode="v"})
583 -- Load default translation
584 require "luci.i18n".loadc("base")
586 local scope = setmetatable({}, {__index = luci.dispatcher})
588 for k, v in pairs(index) do
594 local function modisort(a,b)
595 return modi[a].order < modi[b].order
598 for _, v in util.spairs(modi, modisort) do
599 scope._NAME = v.module
600 setfenv(v.func, scope)
607 --- Register a tree modifier.
608 -- @param func Modifier function
609 -- @param order Modifier order value (optional)
610 function modifier(func, order)
611 context.modifiers[#context.modifiers+1] = {
619 --- Clone a node of the dispatching tree to another position.
620 -- @param path Virtual path destination
621 -- @param clone Virtual path source
622 -- @param title Destination node title (optional)
623 -- @param order Destination node order value (optional)
624 -- @return Dispatching tree node
625 function assign(path, clone, title, order)
626 local obj = node(unpack(path))
633 setmetatable(obj, {__index = _create_node(clone)})
638 --- Create a new dispatching node and define common parameters.
639 -- @param path Virtual path
640 -- @param target Target function to call when dispatched.
641 -- @param title Destination node title
642 -- @param order Destination node order value (optional)
643 -- @return Dispatching tree node
644 function entry(path, target, title, order)
645 local c = node(unpack(path))
650 c.module = getfenv(2)._NAME
655 --- Fetch or create a dispatching node without setting the target module or
656 -- enabling the node.
657 -- @param ... Virtual path
658 -- @return Dispatching tree node
660 return _create_node({...})
663 --- Fetch or create a new dispatching node.
664 -- @param ... Virtual path
665 -- @return Dispatching tree node
667 local c = _create_node({...})
669 c.module = getfenv(2)._NAME
675 function _create_node(path)
680 local name = table.concat(path, ".")
681 local c = context.treecache[name]
684 local last = table.remove(path)
685 local parent = _create_node(path)
687 c = {nodes={}, auto=true}
688 -- the node is "in request" if the request path matches
689 -- at least up to the length of the node path
690 if parent.inreq and context.path[#path+1] == last then
693 parent.nodes[last] = c
694 context.treecache[name] = c
701 function _firstchild()
702 local path = { unpack(context.path) }
703 local name = table.concat(path, ".")
704 local node = context.treecache[name]
707 if node and node.nodes and next(node.nodes) then
709 for k, v in pairs(node.nodes) do
711 (v.order or 100) < (node.nodes[lowest].order or 100)
718 assert(lowest ~= nil,
719 "The requested node contains no childs, unable to redispatch")
721 path[#path+1] = lowest
725 --- Alias the first (lowest order) page automatically
726 function firstchild()
727 return { type = "firstchild", target = _firstchild }
730 --- Create a redirect to another dispatching node.
731 -- @param ... Virtual path destination
735 for _, r in ipairs({...}) do
743 --- Rewrite the first x path values of the request.
744 -- @param n Number of path values to replace
745 -- @param ... Virtual path to replace removed path values with
746 function rewrite(n, ...)
749 local dispatched = util.clone(context.dispatched)
752 table.remove(dispatched, 1)
755 for i, r in ipairs(req) do
756 table.insert(dispatched, i, r)
759 for _, r in ipairs({...}) do
760 dispatched[#dispatched+1] = r
768 local function _call(self, ...)
769 local func = getfenv()[self.name]
771 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
773 assert(type(func) == "function",
774 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
775 'of type "' .. type(func) .. '".')
777 if #self.argv > 0 then
778 return func(unpack(self.argv), ...)
784 --- Create a function-call dispatching target.
785 -- @param name Target function of local controller
786 -- @param ... Additional parameters passed to the function
787 function call(name, ...)
788 return {type = "call", argv = {...}, name = name, target = _call}
792 local _template = function(self, ...)
793 require "luci.template".render(self.view)
796 --- Create a template render dispatching target.
797 -- @param name Template to be rendered
798 function template(name)
799 return {type = "template", view = name, target = _template}
803 local function _cbi(self, ...)
804 local cbi = require "luci.cbi"
805 local tpl = require "luci.template"
806 local http = require "luci.http"
808 local config = self.config or {}
809 local maps = cbi.load(self.model, ...)
813 for i, res in ipairs(maps) do
815 local cstate = res:parse()
816 if cstate and (not state or cstate < state) then
821 local function _resolve_path(path)
822 return type(path) == "table" and build_url(unpack(path)) or path
825 if config.on_valid_to and state and state > 0 and state < 2 then
826 http.redirect(_resolve_path(config.on_valid_to))
830 if config.on_changed_to and state and state > 1 then
831 http.redirect(_resolve_path(config.on_changed_to))
835 if config.on_success_to and state and state > 0 then
836 http.redirect(_resolve_path(config.on_success_to))
840 if config.state_handler then
841 if not config.state_handler(state, maps) then
846 http.header("X-CBI-State", state or 0)
848 if not config.noheader then
849 tpl.render("cbi/header", {state = state})
854 local applymap = false
855 local pageaction = true
856 local parsechain = { }
858 for i, res in ipairs(maps) do
859 if res.apply_needed and res.parsechain then
861 for _, c in ipairs(res.parsechain) do
862 parsechain[#parsechain+1] = c
868 redirect = redirect or res.redirect
871 if res.pageaction == false then
876 messages = messages or { }
877 messages[#messages+1] = res.message
881 for i, res in ipairs(maps) do
887 pageaction = pageaction,
888 parsechain = parsechain
892 if not config.nofooter then
893 tpl.render("cbi/footer", {
895 pageaction = pageaction,
898 autoapply = config.autoapply
903 --- Create a CBI model dispatching target.
904 -- @param model CBI model to be rendered
905 function cbi(model, config)
906 return {type = "cbi", config = config, model = model, target = _cbi}
910 local function _arcombine(self, ...)
912 local target = #argv > 0 and self.targets[2] or self.targets[1]
913 setfenv(target.target, self.env)
914 target:target(unpack(argv))
917 --- Create a combined dispatching target for non argv and argv requests.
918 -- @param trg1 Overview Target
919 -- @param trg2 Detail Target
920 function arcombine(trg1, trg2)
921 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
925 local function _form(self, ...)
926 local cbi = require "luci.cbi"
927 local tpl = require "luci.template"
928 local http = require "luci.http"
930 local maps = luci.cbi.load(self.model, ...)
933 for i, res in ipairs(maps) do
934 local cstate = res:parse()
935 if cstate and (not state or cstate < state) then
940 http.header("X-CBI-State", state or 0)
942 for i, res in ipairs(maps) do
948 --- Create a CBI form model dispatching target.
949 -- @param model CBI form model tpo be rendered
951 return {type = "cbi", model = model, target = _form}
954 --- Access the luci.i18n translate() api.
957 -- @param text Text to translate
958 translate = i18n.translate
960 --- No-op function used to mark translation entries for menu labels.
961 -- This function does not actually translate the given argument but
962 -- is used by build/i18n-scan.pl to find translatable entries.