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("username")
149 local pass = luci.http.formvalue("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 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 sauth = require "luci.sauth"
337 local authen = type(track.sysauth_authenticator) == "function"
338 and track.sysauth_authenticator
339 or authenticator[track.sysauth_authenticator]
341 local def = (type(track.sysauth) == "string") and track.sysauth
342 local accs = def and {track.sysauth} or track.sysauth
343 local sess = ctx.authsession
344 local verifytoken = false
346 sess = luci.http.getcookie("sysauth")
347 sess = sess and sess:match("^[a-f0-9]*$")
351 local sdat = sauth.read(sess)
355 sdat = loadstring(sdat)
358 if not verifytoken or ctx.urltoken.stok == sdat.token then
362 local eu = http.getenv("HTTP_AUTH_USER")
363 local ep = http.getenv("HTTP_AUTH_PASS")
364 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
365 authen = function() return eu end
369 if not util.contains(accs, user) then
371 ctx.urltoken.stok = nil
372 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
373 if not user or not util.contains(accs, user) then
376 local sid = sess or luci.sys.uniqueid(16)
378 local token = luci.sys.uniqueid(16)
379 sauth.write(sid, util.get_bytecode({
382 secret=luci.sys.uniqueid(16)
384 ctx.urltoken.stok = token
386 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
387 ctx.authsession = sid
391 luci.http.status(403, "Forbidden")
395 ctx.authsession = sess
400 if track.setgroup then
401 luci.sys.process.setgroup(track.setgroup)
404 if track.setuser then
405 luci.sys.process.setuser(track.setuser)
410 if type(c.target) == "function" then
412 elseif type(c.target) == "table" then
413 target = c.target.target
417 if c and (c.index or type(target) == "function") then
419 ctx.requested = ctx.requested or ctx.dispatched
422 if c and c.index then
423 local tpl = require "luci.template"
425 if util.copcall(tpl.render, "indexer", {}) then
430 if type(target) == "function" then
431 util.copcall(function()
432 local oldenv = getfenv(target)
433 local module = require(c.module)
434 local env = setmetatable({}, {__index=
437 return rawget(tbl, key) or module[key] or oldenv[key]
444 if type(c.target) == "table" then
445 ok, err = util.copcall(target, c.target, unpack(args))
447 ok, err = util.copcall(target, unpack(args))
450 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
451 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
452 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
455 if not root or not root.target then
456 error404("No root node was registered, this usually happens if no module was installed.\n" ..
457 "Install luci-mod-admin-full and retry. " ..
458 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
460 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
461 "If this url belongs to an extension, make sure it is properly installed.\n" ..
462 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
467 --- Generate the dispatching index using the best possible strategy.
468 function createindex()
469 local path = luci.util.libpath() .. "/controller/"
470 local suff = { ".lua", ".lua.gz" }
472 if luci.util.copcall(require, "luci.fastindex") then
473 createindex_fastindex(path, suff)
475 createindex_plain(path, suff)
479 --- Generate the dispatching index using the fastindex C-indexer.
480 -- @param path Controller base directory
481 -- @param suffixes Controller file suffixes
482 function createindex_fastindex(path, suffixes)
486 fi = luci.fastindex.new("index")
487 for _, suffix in ipairs(suffixes) do
488 fi.add(path .. "*" .. suffix)
489 fi.add(path .. "*/*" .. suffix)
494 for k, v in pairs(fi.indexes) do
499 --- Generate the dispatching index using the native file-cache based strategy.
500 -- @param path Controller base directory
501 -- @param suffixes Controller file suffixes
502 function createindex_plain(path, suffixes)
503 local controllers = { }
504 for _, suffix in ipairs(suffixes) do
505 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
506 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
510 local cachedate = fs.stat(indexcache, "mtime")
513 for _, obj in ipairs(controllers) do
514 local omtime = fs.stat(obj, "mtime")
515 realdate = (omtime and omtime > realdate) and omtime or realdate
518 if cachedate > realdate then
520 sys.process.info("uid") == fs.stat(indexcache, "uid")
521 and fs.stat(indexcache, "modestr") == "rw-------",
522 "Fatal: Indexcache is not sane!"
525 index = loadfile(indexcache)()
533 for i,c in ipairs(controllers) do
534 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
535 for _, suffix in ipairs(suffixes) do
536 modname = modname:gsub(suffix.."$", "")
539 local mod = require(modname)
541 "Invalid controller file found\n" ..
542 "The file '" .. c .. "' contains an invalid module line.\n" ..
543 "Please verify whether the module name is set to '" .. modname ..
544 "' - It must correspond to the file path!")
546 local idx = mod.index
547 assert(type(idx) == "function",
548 "Invalid controller file found\n" ..
549 "The file '" .. c .. "' contains no index() function.\n" ..
550 "Please make sure that the controller contains a valid " ..
551 "index function and verify the spelling!")
557 local f = nixio.open(indexcache, "w", 600)
558 f:writeall(util.get_bytecode(index))
563 --- Create the dispatching tree from the index.
564 -- Build the index before if it does not exist yet.
565 function createtree()
571 local tree = {nodes={}, inreq=true}
574 ctx.treecache = setmetatable({}, {__mode="v"})
578 -- Load default translation
579 require "luci.i18n".loadc("base")
581 local scope = setmetatable({}, {__index = luci.dispatcher})
583 for k, v in pairs(index) do
589 local function modisort(a,b)
590 return modi[a].order < modi[b].order
593 for _, v in util.spairs(modi, modisort) do
594 scope._NAME = v.module
595 setfenv(v.func, scope)
602 --- Register a tree modifier.
603 -- @param func Modifier function
604 -- @param order Modifier order value (optional)
605 function modifier(func, order)
606 context.modifiers[#context.modifiers+1] = {
614 --- Clone a node of the dispatching tree to another position.
615 -- @param path Virtual path destination
616 -- @param clone Virtual path source
617 -- @param title Destination node title (optional)
618 -- @param order Destination node order value (optional)
619 -- @return Dispatching tree node
620 function assign(path, clone, title, order)
621 local obj = node(unpack(path))
628 setmetatable(obj, {__index = _create_node(clone)})
633 --- Create a new dispatching node and define common parameters.
634 -- @param path Virtual path
635 -- @param target Target function to call when dispatched.
636 -- @param title Destination node title
637 -- @param order Destination node order value (optional)
638 -- @return Dispatching tree node
639 function entry(path, target, title, order)
640 local c = node(unpack(path))
645 c.module = getfenv(2)._NAME
650 --- Fetch or create a dispatching node without setting the target module or
651 -- enabling the node.
652 -- @param ... Virtual path
653 -- @return Dispatching tree node
655 return _create_node({...})
658 --- Fetch or create a new dispatching node.
659 -- @param ... Virtual path
660 -- @return Dispatching tree node
662 local c = _create_node({...})
664 c.module = getfenv(2)._NAME
670 function _create_node(path)
675 local name = table.concat(path, ".")
676 local c = context.treecache[name]
679 local last = table.remove(path)
680 local parent = _create_node(path)
682 c = {nodes={}, auto=true}
683 -- the node is "in request" if the request path matches
684 -- at least up to the length of the node path
685 if parent.inreq and context.path[#path+1] == last then
688 parent.nodes[last] = c
689 context.treecache[name] = c
696 function _firstchild()
697 local path = { unpack(context.path) }
698 local name = table.concat(path, ".")
699 local node = context.treecache[name]
702 if node and node.nodes and next(node.nodes) then
704 for k, v in pairs(node.nodes) do
706 (v.order or 100) < (node.nodes[lowest].order or 100)
713 assert(lowest ~= nil,
714 "The requested node contains no childs, unable to redispatch")
716 path[#path+1] = lowest
720 --- Alias the first (lowest order) page automatically
721 function firstchild()
722 return { type = "firstchild", target = _firstchild }
725 --- Create a redirect to another dispatching node.
726 -- @param ... Virtual path destination
730 for _, r in ipairs({...}) do
738 --- Rewrite the first x path values of the request.
739 -- @param n Number of path values to replace
740 -- @param ... Virtual path to replace removed path values with
741 function rewrite(n, ...)
744 local dispatched = util.clone(context.dispatched)
747 table.remove(dispatched, 1)
750 for i, r in ipairs(req) do
751 table.insert(dispatched, i, r)
754 for _, r in ipairs({...}) do
755 dispatched[#dispatched+1] = r
763 local function _call(self, ...)
764 local func = getfenv()[self.name]
766 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
768 assert(type(func) == "function",
769 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
770 'of type "' .. type(func) .. '".')
772 if #self.argv > 0 then
773 return func(unpack(self.argv), ...)
779 --- Create a function-call dispatching target.
780 -- @param name Target function of local controller
781 -- @param ... Additional parameters passed to the function
782 function call(name, ...)
783 return {type = "call", argv = {...}, name = name, target = _call}
787 local _template = function(self, ...)
788 require "luci.template".render(self.view)
791 --- Create a template render dispatching target.
792 -- @param name Template to be rendered
793 function template(name)
794 return {type = "template", view = name, target = _template}
798 local function _cbi(self, ...)
799 local cbi = require "luci.cbi"
800 local tpl = require "luci.template"
801 local http = require "luci.http"
803 local config = self.config or {}
804 local maps = cbi.load(self.model, ...)
808 for i, res in ipairs(maps) do
810 local cstate = res:parse()
811 if cstate and (not state or cstate < state) then
816 local function _resolve_path(path)
817 return type(path) == "table" and build_url(unpack(path)) or path
820 if config.on_valid_to and state and state > 0 and state < 2 then
821 http.redirect(_resolve_path(config.on_valid_to))
825 if config.on_changed_to and state and state > 1 then
826 http.redirect(_resolve_path(config.on_changed_to))
830 if config.on_success_to and state and state > 0 then
831 http.redirect(_resolve_path(config.on_success_to))
835 if config.state_handler then
836 if not config.state_handler(state, maps) then
841 http.header("X-CBI-State", state or 0)
843 if not config.noheader then
844 tpl.render("cbi/header", {state = state})
849 local applymap = false
850 local pageaction = true
851 local parsechain = { }
853 for i, res in ipairs(maps) do
854 if res.apply_needed and res.parsechain then
856 for _, c in ipairs(res.parsechain) do
857 parsechain[#parsechain+1] = c
863 redirect = redirect or res.redirect
866 if res.pageaction == false then
871 messages = messages or { }
872 messages[#messages+1] = res.message
876 for i, res in ipairs(maps) do
882 pageaction = pageaction,
883 parsechain = parsechain
887 if not config.nofooter then
888 tpl.render("cbi/footer", {
890 pageaction = pageaction,
893 autoapply = config.autoapply
898 --- Create a CBI model dispatching target.
899 -- @param model CBI model to be rendered
900 function cbi(model, config)
901 return {type = "cbi", config = config, model = model, target = _cbi}
905 local function _arcombine(self, ...)
907 local target = #argv > 0 and self.targets[2] or self.targets[1]
908 setfenv(target.target, self.env)
909 target:target(unpack(argv))
912 --- Create a combined dispatching target for non argv and argv requests.
913 -- @param trg1 Overview Target
914 -- @param trg2 Detail Target
915 function arcombine(trg1, trg2)
916 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
920 local function _form(self, ...)
921 local cbi = require "luci.cbi"
922 local tpl = require "luci.template"
923 local http = require "luci.http"
925 local maps = luci.cbi.load(self.model, ...)
928 for i, res in ipairs(maps) do
929 local cstate = res:parse()
930 if cstate and (not state or cstate < state) then
935 http.header("X-CBI-State", state or 0)
937 for i, res in ipairs(maps) do
943 --- Create a CBI form model dispatching target.
944 -- @param model CBI form model tpo be rendered
946 return {type = "cbi", model = model, target = _form}
949 --- Access the luci.i18n translate() api.
952 -- @param text Text to translate
953 translate = i18n.translate
955 --- No-op function used to mark translation entries for menu labels.
956 -- This function does not actually translate the given argument but
957 -- is used by build/i18n-scan.pl to find translatable entries.