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 http.status(404, "Not Found")
118 message = message or "Not Found"
120 require("luci.template")
121 if not util.copcall(luci.template.render, "error404") then
122 http.prepare_content("text/plain")
128 --- Send a 500 error code and render the "error500" template if available.
129 -- @param message Custom error message (optional)#
131 function error500(message)
133 if not context.template_header_sent then
134 http.status(500, "Internal Server Error")
135 http.prepare_content("text/plain")
138 require("luci.template")
139 if not util.copcall(luci.template.render, "error500", {message=message}) then
140 http.prepare_content("text/plain")
147 function authenticator.htmlauth(validator, accs, default)
148 local user = http.formvalue("luci_username")
149 local pass = http.formvalue("luci_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 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 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({
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 authen = type(track.sysauth_authenticator) == "function"
337 and track.sysauth_authenticator
338 or authenticator[track.sysauth_authenticator]
340 local def = (type(track.sysauth) == "string") and track.sysauth
341 local accs = def and {track.sysauth} or track.sysauth
342 local sess = ctx.authsession
343 local verifytoken = false
345 sess = http.getcookie("sysauth")
346 sess = sess and sess:match("^[a-f0-9]*$")
350 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
354 if not verifytoken or ctx.urltoken.stok == sdat.token then
358 local eu = http.getenv("HTTP_AUTH_USER")
359 local ep = http.getenv("HTTP_AUTH_PASS")
360 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
361 authen = function() return eu end
365 if not util.contains(accs, user) then
367 ctx.urltoken.stok = nil
368 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
369 if not user or not util.contains(accs, user) then
373 local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime })
375 local token = luci.sys.uniqueid(16)
376 util.ubus("session", "set", {
377 ubus_rpc_session = sdat.ubus_rpc_session,
381 section = luci.sys.uniqueid(16)
384 sess = sdat.ubus_rpc_session
385 ctx.urltoken.stok = token
390 http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
391 ctx.authsession = sess
396 http.status(403, "Forbidden")
400 ctx.authsession = sess
405 if track.setgroup then
406 luci.sys.process.setgroup(track.setgroup)
409 if track.setuser then
410 luci.sys.process.setuser(track.setuser)
415 if type(c.target) == "function" then
417 elseif type(c.target) == "table" then
418 target = c.target.target
422 if c and (c.index or type(target) == "function") then
424 ctx.requested = ctx.requested or ctx.dispatched
427 if c and c.index then
428 local tpl = require "luci.template"
430 if util.copcall(tpl.render, "indexer", {}) then
435 if type(target) == "function" then
436 util.copcall(function()
437 local oldenv = getfenv(target)
438 local module = require(c.module)
439 local env = setmetatable({}, {__index=
442 return rawget(tbl, key) or module[key] or oldenv[key]
449 if type(c.target) == "table" then
450 ok, err = util.copcall(target, c.target, unpack(args))
452 ok, err = util.copcall(target, unpack(args))
455 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
456 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
457 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
460 if not root or not root.target then
461 error404("No root node was registered, this usually happens if no module was installed.\n" ..
462 "Install luci-mod-admin-full and retry. " ..
463 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
465 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
466 "If this url belongs to an extension, make sure it is properly installed.\n" ..
467 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
472 --- Generate the dispatching index using the native file-cache based strategy.
473 function createindex()
474 local controllers = { }
475 local base = "%s/controller/" % util.libpath()
478 for path in (fs.glob("%s*.lua" % base) or function() end) do
479 controllers[#controllers+1] = path
482 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
483 controllers[#controllers+1] = path
487 local cachedate = fs.stat(indexcache, "mtime")
490 for _, obj in ipairs(controllers) do
491 local omtime = fs.stat(obj, "mtime")
492 realdate = (omtime and omtime > realdate) and omtime or realdate
495 if cachedate > realdate and sys.process.info("uid") == 0 then
497 sys.process.info("uid") == fs.stat(indexcache, "uid")
498 and fs.stat(indexcache, "modestr") == "rw-------",
499 "Fatal: Indexcache is not sane!"
502 index = loadfile(indexcache)()
510 for _, path in ipairs(controllers) do
511 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
512 local mod = require(modname)
514 "Invalid controller file found\n" ..
515 "The file '" .. path .. "' contains an invalid module line.\n" ..
516 "Please verify whether the module name is set to '" .. modname ..
517 "' - It must correspond to the file path!")
519 local idx = mod.index
520 assert(type(idx) == "function",
521 "Invalid controller file found\n" ..
522 "The file '" .. path .. "' contains no index() function.\n" ..
523 "Please make sure that the controller contains a valid " ..
524 "index function and verify the spelling!")
530 local f = nixio.open(indexcache, "w", 600)
531 f:writeall(util.get_bytecode(index))
536 --- Create the dispatching tree from the index.
537 -- Build the index before if it does not exist yet.
538 function createtree()
544 local tree = {nodes={}, inreq=true}
547 ctx.treecache = setmetatable({}, {__mode="v"})
551 -- Load default translation
552 require "luci.i18n".loadc("base")
554 local scope = setmetatable({}, {__index = luci.dispatcher})
556 for k, v in pairs(index) do
562 local function modisort(a,b)
563 return modi[a].order < modi[b].order
566 for _, v in util.spairs(modi, modisort) do
567 scope._NAME = v.module
568 setfenv(v.func, scope)
575 --- Register a tree modifier.
576 -- @param func Modifier function
577 -- @param order Modifier order value (optional)
578 function modifier(func, order)
579 context.modifiers[#context.modifiers+1] = {
587 --- Clone a node of the dispatching tree to another position.
588 -- @param path Virtual path destination
589 -- @param clone Virtual path source
590 -- @param title Destination node title (optional)
591 -- @param order Destination node order value (optional)
592 -- @return Dispatching tree node
593 function assign(path, clone, title, order)
594 local obj = node(unpack(path))
601 setmetatable(obj, {__index = _create_node(clone)})
606 --- Create a new dispatching node and define common parameters.
607 -- @param path Virtual path
608 -- @param target Target function to call when dispatched.
609 -- @param title Destination node title
610 -- @param order Destination node order value (optional)
611 -- @return Dispatching tree node
612 function entry(path, target, title, order)
613 local c = node(unpack(path))
618 c.module = getfenv(2)._NAME
623 --- Fetch or create a dispatching node without setting the target module or
624 -- enabling the node.
625 -- @param ... Virtual path
626 -- @return Dispatching tree node
628 return _create_node({...})
631 --- Fetch or create a new dispatching node.
632 -- @param ... Virtual path
633 -- @return Dispatching tree node
635 local c = _create_node({...})
637 c.module = getfenv(2)._NAME
643 function _create_node(path)
648 local name = table.concat(path, ".")
649 local c = context.treecache[name]
652 local last = table.remove(path)
653 local parent = _create_node(path)
655 c = {nodes={}, auto=true}
656 -- the node is "in request" if the request path matches
657 -- at least up to the length of the node path
658 if parent.inreq and context.path[#path+1] == last then
661 parent.nodes[last] = c
662 context.treecache[name] = c
669 function _firstchild()
670 local path = { unpack(context.path) }
671 local name = table.concat(path, ".")
672 local node = context.treecache[name]
675 if node and node.nodes and next(node.nodes) then
677 for k, v in pairs(node.nodes) do
679 (v.order or 100) < (node.nodes[lowest].order or 100)
686 assert(lowest ~= nil,
687 "The requested node contains no childs, unable to redispatch")
689 path[#path+1] = lowest
693 --- Alias the first (lowest order) page automatically
694 function firstchild()
695 return { type = "firstchild", target = _firstchild }
698 --- Create a redirect to another dispatching node.
699 -- @param ... Virtual path destination
703 for _, r in ipairs({...}) do
711 --- Rewrite the first x path values of the request.
712 -- @param n Number of path values to replace
713 -- @param ... Virtual path to replace removed path values with
714 function rewrite(n, ...)
717 local dispatched = util.clone(context.dispatched)
720 table.remove(dispatched, 1)
723 for i, r in ipairs(req) do
724 table.insert(dispatched, i, r)
727 for _, r in ipairs({...}) do
728 dispatched[#dispatched+1] = r
736 local function _call(self, ...)
737 local func = getfenv()[self.name]
739 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
741 assert(type(func) == "function",
742 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
743 'of type "' .. type(func) .. '".')
745 if #self.argv > 0 then
746 return func(unpack(self.argv), ...)
752 --- Create a function-call dispatching target.
753 -- @param name Target function of local controller
754 -- @param ... Additional parameters passed to the function
755 function call(name, ...)
756 return {type = "call", argv = {...}, name = name, target = _call}
760 local _template = function(self, ...)
761 require "luci.template".render(self.view)
764 --- Create a template render dispatching target.
765 -- @param name Template to be rendered
766 function template(name)
767 return {type = "template", view = name, target = _template}
771 local function _cbi(self, ...)
772 local cbi = require "luci.cbi"
773 local tpl = require "luci.template"
774 local http = require "luci.http"
776 local config = self.config or {}
777 local maps = cbi.load(self.model, ...)
781 for i, res in ipairs(maps) do
783 local cstate = res:parse()
784 if cstate and (not state or cstate < state) then
789 local function _resolve_path(path)
790 return type(path) == "table" and build_url(unpack(path)) or path
793 if config.on_valid_to and state and state > 0 and state < 2 then
794 http.redirect(_resolve_path(config.on_valid_to))
798 if config.on_changed_to and state and state > 1 then
799 http.redirect(_resolve_path(config.on_changed_to))
803 if config.on_success_to and state and state > 0 then
804 http.redirect(_resolve_path(config.on_success_to))
808 if config.state_handler then
809 if not config.state_handler(state, maps) then
814 http.header("X-CBI-State", state or 0)
816 if not config.noheader then
817 tpl.render("cbi/header", {state = state})
822 local applymap = false
823 local pageaction = true
824 local parsechain = { }
826 for i, res in ipairs(maps) do
827 if res.apply_needed and res.parsechain then
829 for _, c in ipairs(res.parsechain) do
830 parsechain[#parsechain+1] = c
836 redirect = redirect or res.redirect
839 if res.pageaction == false then
844 messages = messages or { }
845 messages[#messages+1] = res.message
849 for i, res in ipairs(maps) do
855 pageaction = pageaction,
856 parsechain = parsechain
860 if not config.nofooter then
861 tpl.render("cbi/footer", {
863 pageaction = pageaction,
866 autoapply = config.autoapply
871 --- Create a CBI model dispatching target.
872 -- @param model CBI model to be rendered
873 function cbi(model, config)
874 return {type = "cbi", config = config, model = model, target = _cbi}
878 local function _arcombine(self, ...)
880 local target = #argv > 0 and self.targets[2] or self.targets[1]
881 setfenv(target.target, self.env)
882 target:target(unpack(argv))
885 --- Create a combined dispatching target for non argv and argv requests.
886 -- @param trg1 Overview Target
887 -- @param trg2 Detail Target
888 function arcombine(trg1, trg2)
889 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
893 local function _form(self, ...)
894 local cbi = require "luci.cbi"
895 local tpl = require "luci.template"
896 local http = require "luci.http"
898 local maps = luci.cbi.load(self.model, ...)
901 for i, res in ipairs(maps) do
902 local cstate = res:parse()
903 if cstate and (not state or cstate < state) then
908 http.header("X-CBI-State", state or 0)
910 for i, res in ipairs(maps) do
916 --- Create a CBI form model dispatching target.
917 -- @param model CBI form model tpo be rendered
919 return {type = "cbi", model = model, target = _form}
922 --- Access the luci.i18n translate() api.
925 -- @param text Text to translate
926 translate = i18n.translate
928 --- No-op function used to mark translation entries for menu labels.
929 -- This function does not actually translate the given argument but
930 -- is used by build/i18n-scan.pl to find translatable entries.