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(1)
290 return string.format(
291 ' %s="%s"', tostring(key),
292 luci.util.pcdata(tostring( val
293 or (type(env[key]) ~= "function" and env[key])
301 tpl.context.viewns = setmetatable({
302 write = luci.http.write;
303 include = function(name) tpl.Template(name):render(getfenv(2)) end;
304 translate = i18n.translate;
305 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
306 striptags = util.striptags;
307 pcdata = util.pcdata;
309 theme = fs.basename(media);
310 resource = luci.config.main.resourcebase;
312 attr = function(...) return ifattr(true, ...) end
313 }, {__index=function(table, key)
314 if key == "controller" then
316 elseif key == "REQUEST_URI" then
317 return build_url(unpack(ctx.requestpath))
319 return rawget(table, key) or _G[key]
324 track.dependent = (track.dependent ~= false)
325 assert(not track.dependent or not track.auto,
326 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
327 "has no parent node so the access to this location has been denied.\n" ..
328 "This is a software bug, please report this message at " ..
329 "http://luci.subsignal.org/trac/newticket"
332 if track.sysauth then
333 local sauth = require "luci.sauth"
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 = luci.http.getcookie("sysauth")
345 sess = sess and sess:match("^[a-f0-9]*$")
349 local sdat = sauth.read(sess)
353 sdat = loadstring(sdat)
356 if not verifytoken or ctx.urltoken.stok == sdat.token then
360 local eu = http.getenv("HTTP_AUTH_USER")
361 local ep = http.getenv("HTTP_AUTH_PASS")
362 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
363 authen = function() return eu end
367 if not util.contains(accs, user) then
369 ctx.urltoken.stok = nil
370 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
371 if not user or not util.contains(accs, user) then
374 local sid = sess or luci.sys.uniqueid(16)
376 local token = luci.sys.uniqueid(16)
377 sauth.write(sid, util.get_bytecode({
380 secret=luci.sys.uniqueid(16)
382 ctx.urltoken.stok = token
384 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
385 ctx.authsession = sid
389 luci.http.status(403, "Forbidden")
393 ctx.authsession = sess
398 if track.setgroup then
399 luci.sys.process.setgroup(track.setgroup)
402 if track.setuser then
403 luci.sys.process.setuser(track.setuser)
408 if type(c.target) == "function" then
410 elseif type(c.target) == "table" then
411 target = c.target.target
415 if c and (c.index or type(target) == "function") then
417 ctx.requested = ctx.requested or ctx.dispatched
420 if c and c.index then
421 local tpl = require "luci.template"
423 if util.copcall(tpl.render, "indexer", {}) then
428 if type(target) == "function" then
429 util.copcall(function()
430 local oldenv = getfenv(target)
431 local module = require(c.module)
432 local env = setmetatable({}, {__index=
435 return rawget(tbl, key) or module[key] or oldenv[key]
442 if type(c.target) == "table" then
443 ok, err = util.copcall(target, c.target, unpack(args))
445 ok, err = util.copcall(target, unpack(args))
448 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
449 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
450 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
453 if not root or not root.target then
454 error404("No root node was registered, this usually happens if no module was installed.\n" ..
455 "Install luci-mod-admin-full and retry. " ..
456 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
458 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
459 "If this url belongs to an extension, make sure it is properly installed.\n" ..
460 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
465 --- Generate the dispatching index using the best possible strategy.
466 function createindex()
467 local path = luci.util.libpath() .. "/controller/"
468 local suff = { ".lua", ".lua.gz" }
470 if luci.util.copcall(require, "luci.fastindex") then
471 createindex_fastindex(path, suff)
473 createindex_plain(path, suff)
477 --- Generate the dispatching index using the fastindex C-indexer.
478 -- @param path Controller base directory
479 -- @param suffixes Controller file suffixes
480 function createindex_fastindex(path, suffixes)
484 fi = luci.fastindex.new("index")
485 for _, suffix in ipairs(suffixes) do
486 fi.add(path .. "*" .. suffix)
487 fi.add(path .. "*/*" .. suffix)
492 for k, v in pairs(fi.indexes) do
497 --- Generate the dispatching index using the native file-cache based strategy.
498 -- @param path Controller base directory
499 -- @param suffixes Controller file suffixes
500 function createindex_plain(path, suffixes)
501 local controllers = { }
502 for _, suffix in ipairs(suffixes) do
503 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
504 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
508 local cachedate = fs.stat(indexcache, "mtime")
511 for _, obj in ipairs(controllers) do
512 local omtime = fs.stat(obj, "mtime")
513 realdate = (omtime and omtime > realdate) and omtime or realdate
516 if cachedate > realdate then
518 sys.process.info("uid") == fs.stat(indexcache, "uid")
519 and fs.stat(indexcache, "modestr") == "rw-------",
520 "Fatal: Indexcache is not sane!"
523 index = loadfile(indexcache)()
531 for i,c in ipairs(controllers) do
532 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
533 for _, suffix in ipairs(suffixes) do
534 modname = modname:gsub(suffix.."$", "")
537 local mod = require(modname)
539 "Invalid controller file found\n" ..
540 "The file '" .. c .. "' contains an invalid module line.\n" ..
541 "Please verify whether the module name is set to '" .. modname ..
542 "' - It must correspond to the file path!")
544 local idx = mod.index
545 assert(type(idx) == "function",
546 "Invalid controller file found\n" ..
547 "The file '" .. c .. "' contains no index() function.\n" ..
548 "Please make sure that the controller contains a valid " ..
549 "index function and verify the spelling!")
555 local f = nixio.open(indexcache, "w", 600)
556 f:writeall(util.get_bytecode(index))
561 --- Create the dispatching tree from the index.
562 -- Build the index before if it does not exist yet.
563 function createtree()
569 local tree = {nodes={}, inreq=true}
572 ctx.treecache = setmetatable({}, {__mode="v"})
576 -- Load default translation
577 require "luci.i18n".loadc("base")
579 local scope = setmetatable({}, {__index = luci.dispatcher})
581 for k, v in pairs(index) do
587 local function modisort(a,b)
588 return modi[a].order < modi[b].order
591 for _, v in util.spairs(modi, modisort) do
592 scope._NAME = v.module
593 setfenv(v.func, scope)
600 --- Register a tree modifier.
601 -- @param func Modifier function
602 -- @param order Modifier order value (optional)
603 function modifier(func, order)
604 context.modifiers[#context.modifiers+1] = {
612 --- Clone a node of the dispatching tree to another position.
613 -- @param path Virtual path destination
614 -- @param clone Virtual path source
615 -- @param title Destination node title (optional)
616 -- @param order Destination node order value (optional)
617 -- @return Dispatching tree node
618 function assign(path, clone, title, order)
619 local obj = node(unpack(path))
626 setmetatable(obj, {__index = _create_node(clone)})
631 --- Create a new dispatching node and define common parameters.
632 -- @param path Virtual path
633 -- @param target Target function to call when dispatched.
634 -- @param title Destination node title
635 -- @param order Destination node order value (optional)
636 -- @return Dispatching tree node
637 function entry(path, target, title, order)
638 local c = node(unpack(path))
643 c.module = getfenv(2)._NAME
648 --- Fetch or create a dispatching node without setting the target module or
649 -- enabling the node.
650 -- @param ... Virtual path
651 -- @return Dispatching tree node
653 return _create_node({...})
656 --- Fetch or create a new dispatching node.
657 -- @param ... Virtual path
658 -- @return Dispatching tree node
660 local c = _create_node({...})
662 c.module = getfenv(2)._NAME
668 function _create_node(path)
673 local name = table.concat(path, ".")
674 local c = context.treecache[name]
677 local last = table.remove(path)
678 local parent = _create_node(path)
680 c = {nodes={}, auto=true}
681 -- the node is "in request" if the request path matches
682 -- at least up to the length of the node path
683 if parent.inreq and context.path[#path+1] == last then
686 parent.nodes[last] = c
687 context.treecache[name] = c
694 function _firstchild()
695 local path = { unpack(context.path) }
696 local name = table.concat(path, ".")
697 local node = context.treecache[name]
700 if node and node.nodes and next(node.nodes) then
702 for k, v in pairs(node.nodes) do
704 (v.order or 100) < (node.nodes[lowest].order or 100)
711 assert(lowest ~= nil,
712 "The requested node contains no childs, unable to redispatch")
714 path[#path+1] = lowest
718 --- Alias the first (lowest order) page automatically
719 function firstchild()
720 return { type = "firstchild", target = _firstchild }
723 --- Create a redirect to another dispatching node.
724 -- @param ... Virtual path destination
728 for _, r in ipairs({...}) do
736 --- Rewrite the first x path values of the request.
737 -- @param n Number of path values to replace
738 -- @param ... Virtual path to replace removed path values with
739 function rewrite(n, ...)
742 local dispatched = util.clone(context.dispatched)
745 table.remove(dispatched, 1)
748 for i, r in ipairs(req) do
749 table.insert(dispatched, i, r)
752 for _, r in ipairs({...}) do
753 dispatched[#dispatched+1] = r
761 local function _call(self, ...)
762 if #self.argv > 0 then
763 return getfenv()[self.name](unpack(self.argv), ...)
765 return getfenv()[self.name](...)
769 --- Create a function-call dispatching target.
770 -- @param name Target function of local controller
771 -- @param ... Additional parameters passed to the function
772 function call(name, ...)
773 return {type = "call", argv = {...}, name = name, target = _call}
777 local _template = function(self, ...)
778 require "luci.template".render(self.view)
781 --- Create a template render dispatching target.
782 -- @param name Template to be rendered
783 function template(name)
784 return {type = "template", view = name, target = _template}
788 local function _cbi(self, ...)
789 local cbi = require "luci.cbi"
790 local tpl = require "luci.template"
791 local http = require "luci.http"
793 local config = self.config or {}
794 local maps = cbi.load(self.model, ...)
798 for i, res in ipairs(maps) do
800 local cstate = res:parse()
801 if cstate and (not state or cstate < state) then
806 local function _resolve_path(path)
807 return type(path) == "table" and build_url(unpack(path)) or path
810 if config.on_valid_to and state and state > 0 and state < 2 then
811 http.redirect(_resolve_path(config.on_valid_to))
815 if config.on_changed_to and state and state > 1 then
816 http.redirect(_resolve_path(config.on_changed_to))
820 if config.on_success_to and state and state > 0 then
821 http.redirect(_resolve_path(config.on_success_to))
825 if config.state_handler then
826 if not config.state_handler(state, maps) then
831 http.header("X-CBI-State", state or 0)
833 if not config.noheader then
834 tpl.render("cbi/header", {state = state})
839 local applymap = false
840 local pageaction = true
841 local parsechain = { }
843 for i, res in ipairs(maps) do
844 if res.apply_needed and res.parsechain then
846 for _, c in ipairs(res.parsechain) do
847 parsechain[#parsechain+1] = c
853 redirect = redirect or res.redirect
856 if res.pageaction == false then
861 messages = messages or { }
862 messages[#messages+1] = res.message
866 for i, res in ipairs(maps) do
872 pageaction = pageaction,
873 parsechain = parsechain
877 if not config.nofooter then
878 tpl.render("cbi/footer", {
880 pageaction = pageaction,
883 autoapply = config.autoapply
888 --- Create a CBI model dispatching target.
889 -- @param model CBI model to be rendered
890 function cbi(model, config)
891 return {type = "cbi", config = config, model = model, target = _cbi}
895 local function _arcombine(self, ...)
897 local target = #argv > 0 and self.targets[2] or self.targets[1]
898 setfenv(target.target, self.env)
899 target:target(unpack(argv))
902 --- Create a combined dispatching target for non argv and argv requests.
903 -- @param trg1 Overview Target
904 -- @param trg2 Detail Target
905 function arcombine(trg1, trg2)
906 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
910 local function _form(self, ...)
911 local cbi = require "luci.cbi"
912 local tpl = require "luci.template"
913 local http = require "luci.http"
915 local maps = luci.cbi.load(self.model, ...)
918 for i, res in ipairs(maps) do
919 local cstate = res:parse()
920 if cstate and (not state or cstate < state) then
925 http.header("X-CBI-State", state or 0)
927 for i, res in ipairs(maps) do
933 --- Create a CBI form model dispatching target.
934 -- @param model CBI form model tpo be rendered
936 return {type = "cbi", model = model, target = _form}
939 --- Access the luci.i18n translate() api.
942 -- @param text Text to translate
943 translate = i18n.translate
945 --- No-op function used to mark translation entries for menu labels.
946 -- This function does not actually translate the given argument but
947 -- is used by build/i18n-scan.pl to find translatable entries.