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 if not verifytoken or ctx.urltoken.stok == sdat.token then
359 local eu = http.getenv("HTTP_AUTH_USER")
360 local ep = http.getenv("HTTP_AUTH_PASS")
361 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
362 authen = function() return eu end
366 if not util.contains(accs, user) then
368 ctx.urltoken.stok = nil
369 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
370 if not user or not util.contains(accs, user) then
373 local sid = sess or luci.sys.uniqueid(16)
375 local token = luci.sys.uniqueid(16)
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 local func = getfenv()[self.name]
764 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
766 assert(type(func) == "function",
767 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
768 'of type "' .. type(func) .. '".')
770 if #self.argv > 0 then
771 return func(unpack(self.argv), ...)
777 --- Create a function-call dispatching target.
778 -- @param name Target function of local controller
779 -- @param ... Additional parameters passed to the function
780 function call(name, ...)
781 return {type = "call", argv = {...}, name = name, target = _call}
785 local _template = function(self, ...)
786 require "luci.template".render(self.view)
789 --- Create a template render dispatching target.
790 -- @param name Template to be rendered
791 function template(name)
792 return {type = "template", view = name, target = _template}
796 local function _cbi(self, ...)
797 local cbi = require "luci.cbi"
798 local tpl = require "luci.template"
799 local http = require "luci.http"
801 local config = self.config or {}
802 local maps = cbi.load(self.model, ...)
806 for i, res in ipairs(maps) do
808 local cstate = res:parse()
809 if cstate and (not state or cstate < state) then
814 local function _resolve_path(path)
815 return type(path) == "table" and build_url(unpack(path)) or path
818 if config.on_valid_to and state and state > 0 and state < 2 then
819 http.redirect(_resolve_path(config.on_valid_to))
823 if config.on_changed_to and state and state > 1 then
824 http.redirect(_resolve_path(config.on_changed_to))
828 if config.on_success_to and state and state > 0 then
829 http.redirect(_resolve_path(config.on_success_to))
833 if config.state_handler then
834 if not config.state_handler(state, maps) then
839 http.header("X-CBI-State", state or 0)
841 if not config.noheader then
842 tpl.render("cbi/header", {state = state})
847 local applymap = false
848 local pageaction = true
849 local parsechain = { }
851 for i, res in ipairs(maps) do
852 if res.apply_needed and res.parsechain then
854 for _, c in ipairs(res.parsechain) do
855 parsechain[#parsechain+1] = c
861 redirect = redirect or res.redirect
864 if res.pageaction == false then
869 messages = messages or { }
870 messages[#messages+1] = res.message
874 for i, res in ipairs(maps) do
880 pageaction = pageaction,
881 parsechain = parsechain
885 if not config.nofooter then
886 tpl.render("cbi/footer", {
888 pageaction = pageaction,
891 autoapply = config.autoapply
896 --- Create a CBI model dispatching target.
897 -- @param model CBI model to be rendered
898 function cbi(model, config)
899 return {type = "cbi", config = config, model = model, target = _cbi}
903 local function _arcombine(self, ...)
905 local target = #argv > 0 and self.targets[2] or self.targets[1]
906 setfenv(target.target, self.env)
907 target:target(unpack(argv))
910 --- Create a combined dispatching target for non argv and argv requests.
911 -- @param trg1 Overview Target
912 -- @param trg2 Detail Target
913 function arcombine(trg1, trg2)
914 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
918 local function _form(self, ...)
919 local cbi = require "luci.cbi"
920 local tpl = require "luci.template"
921 local http = require "luci.http"
923 local maps = luci.cbi.load(self.model, ...)
926 for i, res in ipairs(maps) do
927 local cstate = res:parse()
928 if cstate and (not state or cstate < state) then
933 http.header("X-CBI-State", state or 0)
935 for i, res in ipairs(maps) do
941 --- Create a CBI form model dispatching target.
942 -- @param model CBI form model tpo be rendered
944 return {type = "cbi", model = model, target = _form}
947 --- Access the luci.i18n translate() api.
950 -- @param text Text to translate
951 translate = i18n.translate
953 --- No-op function used to mark translation entries for menu labels.
954 -- This function does not actually translate the given argument but
955 -- is used by build/i18n-scan.pl to find translatable entries.