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 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 sauth = require "luci.sauth"
338 local authen = type(track.sysauth_authenticator) == "function"
339 and track.sysauth_authenticator
340 or authenticator[track.sysauth_authenticator]
342 local def = (type(track.sysauth) == "string") and track.sysauth
343 local accs = def and {track.sysauth} or track.sysauth
344 local sess = ctx.authsession
345 local verifytoken = false
347 sess = luci.http.getcookie("sysauth")
348 sess = sess and sess:match("^[a-f0-9]*$")
352 local sdat = sauth.read(sess)
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)
381 secret=luci.sys.uniqueid(16)
383 ctx.urltoken.stok = token
385 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
386 ctx.authsession = sid
390 luci.http.status(403, "Forbidden")
394 ctx.authsession = sess
399 if track.setgroup then
400 luci.sys.process.setgroup(track.setgroup)
403 if track.setuser then
404 luci.sys.process.setuser(track.setuser)
409 if type(c.target) == "function" then
411 elseif type(c.target) == "table" then
412 target = c.target.target
416 if c and (c.index or type(target) == "function") then
418 ctx.requested = ctx.requested or ctx.dispatched
421 if c and c.index then
422 local tpl = require "luci.template"
424 if util.copcall(tpl.render, "indexer", {}) then
429 if type(target) == "function" then
430 util.copcall(function()
431 local oldenv = getfenv(target)
432 local module = require(c.module)
433 local env = setmetatable({}, {__index=
436 return rawget(tbl, key) or module[key] or oldenv[key]
443 if type(c.target) == "table" then
444 ok, err = util.copcall(target, c.target, unpack(args))
446 ok, err = util.copcall(target, unpack(args))
449 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
450 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
451 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
454 if not root or not root.target then
455 error404("No root node was registered, this usually happens if no module was installed.\n" ..
456 "Install luci-mod-admin-full and retry. " ..
457 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
459 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
460 "If this url belongs to an extension, make sure it is properly installed.\n" ..
461 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
466 --- Generate the dispatching index using the best possible strategy.
467 function createindex()
468 local path = luci.util.libpath() .. "/controller/"
469 local suff = { ".lua", ".lua.gz" }
471 if luci.util.copcall(require, "luci.fastindex") then
472 createindex_fastindex(path, suff)
474 createindex_plain(path, suff)
478 --- Generate the dispatching index using the fastindex C-indexer.
479 -- @param path Controller base directory
480 -- @param suffixes Controller file suffixes
481 function createindex_fastindex(path, suffixes)
485 fi = luci.fastindex.new("index")
486 for _, suffix in ipairs(suffixes) do
487 fi.add(path .. "*" .. suffix)
488 fi.add(path .. "*/*" .. suffix)
493 for k, v in pairs(fi.indexes) do
498 --- Generate the dispatching index using the native file-cache based strategy.
499 -- @param path Controller base directory
500 -- @param suffixes Controller file suffixes
501 function createindex_plain(path, suffixes)
502 local controllers = { }
503 for _, suffix in ipairs(suffixes) do
504 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
505 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
509 local cachedate = fs.stat(indexcache, "mtime")
512 for _, obj in ipairs(controllers) do
513 local omtime = fs.stat(obj, "mtime")
514 realdate = (omtime and omtime > realdate) and omtime or realdate
517 if cachedate > realdate then
519 sys.process.info("uid") == fs.stat(indexcache, "uid")
520 and fs.stat(indexcache, "modestr") == "rw-------",
521 "Fatal: Indexcache is not sane!"
524 index = loadfile(indexcache)()
532 for i,c in ipairs(controllers) do
533 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
534 for _, suffix in ipairs(suffixes) do
535 modname = modname:gsub(suffix.."$", "")
538 local mod = require(modname)
540 "Invalid controller file found\n" ..
541 "The file '" .. c .. "' contains an invalid module line.\n" ..
542 "Please verify whether the module name is set to '" .. modname ..
543 "' - It must correspond to the file path!")
545 local idx = mod.index
546 assert(type(idx) == "function",
547 "Invalid controller file found\n" ..
548 "The file '" .. c .. "' contains no index() function.\n" ..
549 "Please make sure that the controller contains a valid " ..
550 "index function and verify the spelling!")
556 local f = nixio.open(indexcache, "w", 600)
557 f:writeall(util.get_bytecode(index))
562 --- Create the dispatching tree from the index.
563 -- Build the index before if it does not exist yet.
564 function createtree()
570 local tree = {nodes={}, inreq=true}
573 ctx.treecache = setmetatable({}, {__mode="v"})
577 -- Load default translation
578 require "luci.i18n".loadc("base")
580 local scope = setmetatable({}, {__index = luci.dispatcher})
582 for k, v in pairs(index) do
588 local function modisort(a,b)
589 return modi[a].order < modi[b].order
592 for _, v in util.spairs(modi, modisort) do
593 scope._NAME = v.module
594 setfenv(v.func, scope)
601 --- Register a tree modifier.
602 -- @param func Modifier function
603 -- @param order Modifier order value (optional)
604 function modifier(func, order)
605 context.modifiers[#context.modifiers+1] = {
613 --- Clone a node of the dispatching tree to another position.
614 -- @param path Virtual path destination
615 -- @param clone Virtual path source
616 -- @param title Destination node title (optional)
617 -- @param order Destination node order value (optional)
618 -- @return Dispatching tree node
619 function assign(path, clone, title, order)
620 local obj = node(unpack(path))
627 setmetatable(obj, {__index = _create_node(clone)})
632 --- Create a new dispatching node and define common parameters.
633 -- @param path Virtual path
634 -- @param target Target function to call when dispatched.
635 -- @param title Destination node title
636 -- @param order Destination node order value (optional)
637 -- @return Dispatching tree node
638 function entry(path, target, title, order)
639 local c = node(unpack(path))
644 c.module = getfenv(2)._NAME
649 --- Fetch or create a dispatching node without setting the target module or
650 -- enabling the node.
651 -- @param ... Virtual path
652 -- @return Dispatching tree node
654 return _create_node({...})
657 --- Fetch or create a new dispatching node.
658 -- @param ... Virtual path
659 -- @return Dispatching tree node
661 local c = _create_node({...})
663 c.module = getfenv(2)._NAME
669 function _create_node(path)
674 local name = table.concat(path, ".")
675 local c = context.treecache[name]
678 local last = table.remove(path)
679 local parent = _create_node(path)
681 c = {nodes={}, auto=true}
682 -- the node is "in request" if the request path matches
683 -- at least up to the length of the node path
684 if parent.inreq and context.path[#path+1] == last then
687 parent.nodes[last] = c
688 context.treecache[name] = c
695 function _firstchild()
696 local path = { unpack(context.path) }
697 local name = table.concat(path, ".")
698 local node = context.treecache[name]
701 if node and node.nodes and next(node.nodes) then
703 for k, v in pairs(node.nodes) do
705 (v.order or 100) < (node.nodes[lowest].order or 100)
712 assert(lowest ~= nil,
713 "The requested node contains no childs, unable to redispatch")
715 path[#path+1] = lowest
719 --- Alias the first (lowest order) page automatically
720 function firstchild()
721 return { type = "firstchild", target = _firstchild }
724 --- Create a redirect to another dispatching node.
725 -- @param ... Virtual path destination
729 for _, r in ipairs({...}) do
737 --- Rewrite the first x path values of the request.
738 -- @param n Number of path values to replace
739 -- @param ... Virtual path to replace removed path values with
740 function rewrite(n, ...)
743 local dispatched = util.clone(context.dispatched)
746 table.remove(dispatched, 1)
749 for i, r in ipairs(req) do
750 table.insert(dispatched, i, r)
753 for _, r in ipairs({...}) do
754 dispatched[#dispatched+1] = r
762 local function _call(self, ...)
763 local func = getfenv()[self.name]
765 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
767 assert(type(func) == "function",
768 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
769 'of type "' .. type(func) .. '".')
771 if #self.argv > 0 then
772 return func(unpack(self.argv), ...)
778 --- Create a function-call dispatching target.
779 -- @param name Target function of local controller
780 -- @param ... Additional parameters passed to the function
781 function call(name, ...)
782 return {type = "call", argv = {...}, name = name, target = _call}
786 local _template = function(self, ...)
787 require "luci.template".render(self.view)
790 --- Create a template render dispatching target.
791 -- @param name Template to be rendered
792 function template(name)
793 return {type = "template", view = name, target = _template}
797 local function _cbi(self, ...)
798 local cbi = require "luci.cbi"
799 local tpl = require "luci.template"
800 local http = require "luci.http"
802 local config = self.config or {}
803 local maps = cbi.load(self.model, ...)
807 for i, res in ipairs(maps) do
809 local cstate = res:parse()
810 if cstate and (not state or cstate < state) then
815 local function _resolve_path(path)
816 return type(path) == "table" and build_url(unpack(path)) or path
819 if config.on_valid_to and state and state > 0 and state < 2 then
820 http.redirect(_resolve_path(config.on_valid_to))
824 if config.on_changed_to and state and state > 1 then
825 http.redirect(_resolve_path(config.on_changed_to))
829 if config.on_success_to and state and state > 0 then
830 http.redirect(_resolve_path(config.on_success_to))
834 if config.state_handler then
835 if not config.state_handler(state, maps) then
840 http.header("X-CBI-State", state or 0)
842 if not config.noheader then
843 tpl.render("cbi/header", {state = state})
848 local applymap = false
849 local pageaction = true
850 local parsechain = { }
852 for i, res in ipairs(maps) do
853 if res.apply_needed and res.parsechain then
855 for _, c in ipairs(res.parsechain) do
856 parsechain[#parsechain+1] = c
862 redirect = redirect or res.redirect
865 if res.pageaction == false then
870 messages = messages or { }
871 messages[#messages+1] = res.message
875 for i, res in ipairs(maps) do
881 pageaction = pageaction,
882 parsechain = parsechain
886 if not config.nofooter then
887 tpl.render("cbi/footer", {
889 pageaction = pageaction,
892 autoapply = config.autoapply
897 --- Create a CBI model dispatching target.
898 -- @param model CBI model to be rendered
899 function cbi(model, config)
900 return {type = "cbi", config = config, model = model, target = _cbi}
904 local function _arcombine(self, ...)
906 local target = #argv > 0 and self.targets[2] or self.targets[1]
907 setfenv(target.target, self.env)
908 target:target(unpack(argv))
911 --- Create a combined dispatching target for non argv and argv requests.
912 -- @param trg1 Overview Target
913 -- @param trg2 Detail Target
914 function arcombine(trg1, trg2)
915 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
919 local function _form(self, ...)
920 local cbi = require "luci.cbi"
921 local tpl = require "luci.template"
922 local http = require "luci.http"
924 local maps = luci.cbi.load(self.model, ...)
927 for i, res in ipairs(maps) do
928 local cstate = res:parse()
929 if cstate and (not state or cstate < state) then
934 http.header("X-CBI-State", state or 0)
936 for i, res in ipairs(maps) do
942 --- Create a CBI form model dispatching target.
943 -- @param model CBI form model tpo be rendered
945 return {type = "cbi", model = model, target = _form}
948 --- Access the luci.i18n translate() api.
951 -- @param text Text to translate
952 translate = i18n.translate
954 --- No-op function used to mark translation entries for menu labels.
955 -- This function does not actually translate the given argument but
956 -- is used by build/i18n-scan.pl to find translatable entries.