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)
97 for k, v in util.spairs(node.nodes,
99 return (node.nodes[a].order or 100) < (node.nodes[b].order or 100)
102 if node_visible(v) then
110 --- Send a 404 error code and render the "error404" template if available.
111 -- @param message Custom error message (optional)
113 function error404(message)
114 luci.http.status(404, "Not Found")
115 message = message or "Not Found"
117 require("luci.template")
118 if not luci.util.copcall(luci.template.render, "error404") then
119 luci.http.prepare_content("text/plain")
120 luci.http.write(message)
125 --- Send a 500 error code and render the "error500" template if available.
126 -- @param message Custom error message (optional)#
128 function error500(message)
129 luci.util.perror(message)
130 if not context.template_header_sent then
131 luci.http.status(500, "Internal Server Error")
132 luci.http.prepare_content("text/plain")
133 luci.http.write(message)
135 require("luci.template")
136 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
137 luci.http.prepare_content("text/plain")
138 luci.http.write(message)
144 function authenticator.htmlauth(validator, accs, default)
145 local user = luci.http.formvalue("username")
146 local pass = luci.http.formvalue("password")
148 if user and validator(user, pass) then
153 require("luci.template")
155 luci.template.render("sysauth", {duser=default, fuser=user})
160 --- Dispatch an HTTP request.
161 -- @param request LuCI HTTP Request object
162 function httpdispatch(request, prefix)
163 luci.http.context.request = request
167 context.urltoken = {}
169 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
172 for _, node in ipairs(prefix) do
177 local tokensok = true
178 for node in pathinfo:gmatch("[^/]+") do
181 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
184 context.urltoken[tkey] = tval
191 local stat, err = util.coxpcall(function()
192 dispatch(context.request)
197 --context._disable_memtrace()
200 --- Dispatches a LuCI virtual path.
201 -- @param request Virtual path
202 function dispatch(request)
203 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
207 local conf = require "luci.config"
209 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
211 local lang = conf.main.lang or "auto"
212 if lang == "auto" then
213 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
214 for lpat in aclang:gmatch("[%w-]+") do
215 lpat = lpat and lpat:gsub("-", "_")
216 if conf.languages[lpat] then
222 require "luci.i18n".setlanguage(lang)
233 ctx.requestargs = ctx.requestargs or args
235 local token = ctx.urltoken
239 for i, s in ipairs(request) do
248 util.update(track, c)
256 for j=n+1, #request do
257 args[#args+1] = request[j]
258 freq[#freq+1] = request[j]
262 ctx.requestpath = ctx.requestpath or freq
266 i18n.loadc(track.i18n)
269 -- Init template engine
270 if (c and c.index) or not track.notemplate then
271 local tpl = require("luci.template")
272 local media = track.mediaurlbase or luci.config.main.mediaurlbase
273 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
275 for name, theme in pairs(luci.config.themes) do
276 if name:sub(1,1) ~= "." and pcall(tpl.Template,
277 "themes/%s/header" % fs.basename(theme)) then
281 assert(media, "No valid theme found")
284 local function ifattr(cond, key, val)
286 local env = getfenv(1)
287 return string.format(
288 ' %s="%s"', tostring(key),
289 luci.util.pcdata(tostring( val
290 or (type(env[key]) ~= "function" and env[key])
298 tpl.context.viewns = setmetatable({
299 write = luci.http.write;
300 include = function(name) tpl.Template(name):render(getfenv(2)) end;
301 translate = i18n.translate;
302 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
303 striptags = util.striptags;
304 pcdata = util.pcdata;
306 theme = fs.basename(media);
307 resource = luci.config.main.resourcebase;
309 attr = function(...) return ifattr(true, ...) end
310 }, {__index=function(table, key)
311 if key == "controller" then
313 elseif key == "REQUEST_URI" then
314 return build_url(unpack(ctx.requestpath))
316 return rawget(table, key) or _G[key]
321 track.dependent = (track.dependent ~= false)
322 assert(not track.dependent or not track.auto,
323 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
324 "has no parent node so the access to this location has been denied.\n" ..
325 "This is a software bug, please report this message at " ..
326 "http://luci.subsignal.org/trac/newticket"
329 if track.sysauth then
330 local sauth = require "luci.sauth"
332 local authen = type(track.sysauth_authenticator) == "function"
333 and track.sysauth_authenticator
334 or authenticator[track.sysauth_authenticator]
336 local def = (type(track.sysauth) == "string") and track.sysauth
337 local accs = def and {track.sysauth} or track.sysauth
338 local sess = ctx.authsession
339 local verifytoken = false
341 sess = luci.http.getcookie("sysauth")
342 sess = sess and sess:match("^[a-f0-9]*$")
346 local sdat = sauth.read(sess)
350 sdat = loadstring(sdat)
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 luci.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(luci.sys.user.checkpasswd, accs, def)
368 if not user or not util.contains(accs, user) then
371 local sid = sess or luci.sys.uniqueid(16)
373 local token = luci.sys.uniqueid(16)
374 sauth.write(sid, util.get_bytecode({
377 secret=luci.sys.uniqueid(16)
379 ctx.urltoken.stok = token
381 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
382 ctx.authsession = sid
386 luci.http.status(403, "Forbidden")
390 ctx.authsession = sess
395 if track.setgroup then
396 luci.sys.process.setgroup(track.setgroup)
399 if track.setuser then
400 luci.sys.process.setuser(track.setuser)
405 if type(c.target) == "function" then
407 elseif type(c.target) == "table" then
408 target = c.target.target
412 if c and (c.index or type(target) == "function") then
414 ctx.requested = ctx.requested or ctx.dispatched
417 if c and c.index then
418 local tpl = require "luci.template"
420 if util.copcall(tpl.render, "indexer", {}) then
425 if type(target) == "function" then
426 util.copcall(function()
427 local oldenv = getfenv(target)
428 local module = require(c.module)
429 local env = setmetatable({}, {__index=
432 return rawget(tbl, key) or module[key] or oldenv[key]
439 if type(c.target) == "table" then
440 ok, err = util.copcall(target, c.target, unpack(args))
442 ok, err = util.copcall(target, unpack(args))
445 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
446 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
447 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
450 if not root or not root.target then
451 error404("No root node was registered, this usually happens if no module was installed.\n" ..
452 "Install luci-mod-admin-full and retry. " ..
453 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
455 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
456 "If this url belongs to an extension, make sure it is properly installed.\n" ..
457 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
462 --- Generate the dispatching index using the best possible strategy.
463 function createindex()
464 local path = luci.util.libpath() .. "/controller/"
465 local suff = { ".lua", ".lua.gz" }
467 if luci.util.copcall(require, "luci.fastindex") then
468 createindex_fastindex(path, suff)
470 createindex_plain(path, suff)
474 --- Generate the dispatching index using the fastindex C-indexer.
475 -- @param path Controller base directory
476 -- @param suffixes Controller file suffixes
477 function createindex_fastindex(path, suffixes)
481 fi = luci.fastindex.new("index")
482 for _, suffix in ipairs(suffixes) do
483 fi.add(path .. "*" .. suffix)
484 fi.add(path .. "*/*" .. suffix)
489 for k, v in pairs(fi.indexes) do
494 --- Generate the dispatching index using the native file-cache based strategy.
495 -- @param path Controller base directory
496 -- @param suffixes Controller file suffixes
497 function createindex_plain(path, suffixes)
498 local controllers = { }
499 for _, suffix in ipairs(suffixes) do
500 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
501 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
505 local cachedate = fs.stat(indexcache, "mtime")
508 for _, obj in ipairs(controllers) do
509 local omtime = fs.stat(obj, "mtime")
510 realdate = (omtime and omtime > realdate) and omtime or realdate
513 if cachedate > realdate then
515 sys.process.info("uid") == fs.stat(indexcache, "uid")
516 and fs.stat(indexcache, "modestr") == "rw-------",
517 "Fatal: Indexcache is not sane!"
520 index = loadfile(indexcache)()
528 for i,c in ipairs(controllers) do
529 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
530 for _, suffix in ipairs(suffixes) do
531 modname = modname:gsub(suffix.."$", "")
534 local mod = require(modname)
536 "Invalid controller file found\n" ..
537 "The file '" .. c .. "' contains an invalid module line.\n" ..
538 "Please verify whether the module name is set to '" .. modname ..
539 "' - It must correspond to the file path!")
541 local idx = mod.index
542 assert(type(idx) == "function",
543 "Invalid controller file found\n" ..
544 "The file '" .. c .. "' contains no index() function.\n" ..
545 "Please make sure that the controller contains a valid " ..
546 "index function and verify the spelling!")
552 local f = nixio.open(indexcache, "w", 600)
553 f:writeall(util.get_bytecode(index))
558 --- Create the dispatching tree from the index.
559 -- Build the index before if it does not exist yet.
560 function createtree()
566 local tree = {nodes={}, inreq=true}
569 ctx.treecache = setmetatable({}, {__mode="v"})
573 -- Load default translation
574 require "luci.i18n".loadc("base")
576 local scope = setmetatable({}, {__index = luci.dispatcher})
578 for k, v in pairs(index) do
584 local function modisort(a,b)
585 return modi[a].order < modi[b].order
588 for _, v in util.spairs(modi, modisort) do
589 scope._NAME = v.module
590 setfenv(v.func, scope)
597 --- Register a tree modifier.
598 -- @param func Modifier function
599 -- @param order Modifier order value (optional)
600 function modifier(func, order)
601 context.modifiers[#context.modifiers+1] = {
609 --- Clone a node of the dispatching tree to another position.
610 -- @param path Virtual path destination
611 -- @param clone Virtual path source
612 -- @param title Destination node title (optional)
613 -- @param order Destination node order value (optional)
614 -- @return Dispatching tree node
615 function assign(path, clone, title, order)
616 local obj = node(unpack(path))
623 setmetatable(obj, {__index = _create_node(clone)})
628 --- Create a new dispatching node and define common parameters.
629 -- @param path Virtual path
630 -- @param target Target function to call when dispatched.
631 -- @param title Destination node title
632 -- @param order Destination node order value (optional)
633 -- @return Dispatching tree node
634 function entry(path, target, title, order)
635 local c = node(unpack(path))
640 c.module = getfenv(2)._NAME
645 --- Fetch or create a dispatching node without setting the target module or
646 -- enabling the node.
647 -- @param ... Virtual path
648 -- @return Dispatching tree node
650 return _create_node({...})
653 --- Fetch or create a new dispatching node.
654 -- @param ... Virtual path
655 -- @return Dispatching tree node
657 local c = _create_node({...})
659 c.module = getfenv(2)._NAME
665 function _create_node(path)
670 local name = table.concat(path, ".")
671 local c = context.treecache[name]
674 local last = table.remove(path)
675 local parent = _create_node(path)
677 c = {nodes={}, auto=true}
678 -- the node is "in request" if the request path matches
679 -- at least up to the length of the node path
680 if parent.inreq and context.path[#path+1] == last then
683 parent.nodes[last] = c
684 context.treecache[name] = c
691 function _firstchild()
692 local path = { unpack(context.path) }
693 local name = table.concat(path, ".")
694 local node = context.treecache[name]
697 if node and node.nodes and next(node.nodes) then
699 for k, v in pairs(node.nodes) do
701 (v.order or 100) < (node.nodes[lowest].order or 100)
708 assert(lowest ~= nil,
709 "The requested node contains no childs, unable to redispatch")
711 path[#path+1] = lowest
715 --- Alias the first (lowest order) page automatically
716 function firstchild()
717 return { type = "firstchild", target = _firstchild }
720 --- Create a redirect to another dispatching node.
721 -- @param ... Virtual path destination
725 for _, r in ipairs({...}) do
733 --- Rewrite the first x path values of the request.
734 -- @param n Number of path values to replace
735 -- @param ... Virtual path to replace removed path values with
736 function rewrite(n, ...)
739 local dispatched = util.clone(context.dispatched)
742 table.remove(dispatched, 1)
745 for i, r in ipairs(req) do
746 table.insert(dispatched, i, r)
749 for _, r in ipairs({...}) do
750 dispatched[#dispatched+1] = r
758 local function _call(self, ...)
759 if #self.argv > 0 then
760 return getfenv()[self.name](unpack(self.argv), ...)
762 return getfenv()[self.name](...)
766 --- Create a function-call dispatching target.
767 -- @param name Target function of local controller
768 -- @param ... Additional parameters passed to the function
769 function call(name, ...)
770 return {type = "call", argv = {...}, name = name, target = _call}
774 local _template = function(self, ...)
775 require "luci.template".render(self.view)
778 --- Create a template render dispatching target.
779 -- @param name Template to be rendered
780 function template(name)
781 return {type = "template", view = name, target = _template}
785 local function _cbi(self, ...)
786 local cbi = require "luci.cbi"
787 local tpl = require "luci.template"
788 local http = require "luci.http"
790 local config = self.config or {}
791 local maps = cbi.load(self.model, ...)
795 for i, res in ipairs(maps) do
797 local cstate = res:parse()
798 if cstate and (not state or cstate < state) then
803 local function _resolve_path(path)
804 return type(path) == "table" and build_url(unpack(path)) or path
807 if config.on_valid_to and state and state > 0 and state < 2 then
808 http.redirect(_resolve_path(config.on_valid_to))
812 if config.on_changed_to and state and state > 1 then
813 http.redirect(_resolve_path(config.on_changed_to))
817 if config.on_success_to and state and state > 0 then
818 http.redirect(_resolve_path(config.on_success_to))
822 if config.state_handler then
823 if not config.state_handler(state, maps) then
828 http.header("X-CBI-State", state or 0)
830 if not config.noheader then
831 tpl.render("cbi/header", {state = state})
836 local applymap = false
837 local pageaction = true
838 local parsechain = { }
840 for i, res in ipairs(maps) do
841 if res.apply_needed and res.parsechain then
843 for _, c in ipairs(res.parsechain) do
844 parsechain[#parsechain+1] = c
850 redirect = redirect or res.redirect
853 if res.pageaction == false then
858 messages = messages or { }
859 messages[#messages+1] = res.message
863 for i, res in ipairs(maps) do
869 pageaction = pageaction,
870 parsechain = parsechain
874 if not config.nofooter then
875 tpl.render("cbi/footer", {
877 pageaction = pageaction,
880 autoapply = config.autoapply
885 --- Create a CBI model dispatching target.
886 -- @param model CBI model to be rendered
887 function cbi(model, config)
888 return {type = "cbi", config = config, model = model, target = _cbi}
892 local function _arcombine(self, ...)
894 local target = #argv > 0 and self.targets[2] or self.targets[1]
895 setfenv(target.target, self.env)
896 target:target(unpack(argv))
899 --- Create a combined dispatching target for non argv and argv requests.
900 -- @param trg1 Overview Target
901 -- @param trg2 Detail Target
902 function arcombine(trg1, trg2)
903 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
907 local function _form(self, ...)
908 local cbi = require "luci.cbi"
909 local tpl = require "luci.template"
910 local http = require "luci.http"
912 local maps = luci.cbi.load(self.model, ...)
915 for i, res in ipairs(maps) do
916 local cstate = res:parse()
917 if cstate and (not state or cstate < state) then
922 http.header("X-CBI-State", state or 0)
924 for i, res in ipairs(maps) do
930 --- Create a CBI form model dispatching target.
931 -- @param model CBI form model tpo be rendered
933 return {type = "cbi", model = model, target = _form}
936 --- Access the luci.i18n translate() api.
939 -- @param text Text to translate
940 translate = i18n.translate
942 --- No-op function used to mark translation entries for menu labels.
943 -- This function does not actually translate the given argument but
944 -- is used by build/i18n-scan.pl to find translatable entries.