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 tpl.context.viewns = setmetatable({
285 write = luci.http.write;
286 include = function(name) tpl.Template(name):render(getfenv(2)) end;
287 translate = i18n.translate;
288 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
289 striptags = util.striptags;
290 pcdata = util.pcdata;
292 theme = fs.basename(media);
293 resource = luci.config.main.resourcebase
294 }, {__index=function(table, key)
295 if key == "controller" then
297 elseif key == "REQUEST_URI" then
298 return build_url(unpack(ctx.requestpath))
300 return rawget(table, key) or _G[key]
305 track.dependent = (track.dependent ~= false)
306 assert(not track.dependent or not track.auto,
307 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
308 "has no parent node so the access to this location has been denied.\n" ..
309 "This is a software bug, please report this message at " ..
310 "http://luci.subsignal.org/trac/newticket"
313 if track.sysauth then
314 local sauth = require "luci.sauth"
316 local authen = type(track.sysauth_authenticator) == "function"
317 and track.sysauth_authenticator
318 or authenticator[track.sysauth_authenticator]
320 local def = (type(track.sysauth) == "string") and track.sysauth
321 local accs = def and {track.sysauth} or track.sysauth
322 local sess = ctx.authsession
323 local verifytoken = false
325 sess = luci.http.getcookie("sysauth")
326 sess = sess and sess:match("^[a-f0-9]*$")
330 local sdat = sauth.read(sess)
334 sdat = loadstring(sdat)
337 if not verifytoken or ctx.urltoken.stok == sdat.token then
341 local eu = http.getenv("HTTP_AUTH_USER")
342 local ep = http.getenv("HTTP_AUTH_PASS")
343 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
344 authen = function() return eu end
348 if not util.contains(accs, user) then
350 ctx.urltoken.stok = nil
351 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
352 if not user or not util.contains(accs, user) then
355 local sid = sess or luci.sys.uniqueid(16)
357 local token = luci.sys.uniqueid(16)
358 sauth.write(sid, util.get_bytecode({
361 secret=luci.sys.uniqueid(16)
363 ctx.urltoken.stok = token
365 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
366 ctx.authsession = sid
370 luci.http.status(403, "Forbidden")
374 ctx.authsession = sess
379 if track.setgroup then
380 luci.sys.process.setgroup(track.setgroup)
383 if track.setuser then
384 luci.sys.process.setuser(track.setuser)
389 if type(c.target) == "function" then
391 elseif type(c.target) == "table" then
392 target = c.target.target
396 if c and (c.index or type(target) == "function") then
398 ctx.requested = ctx.requested or ctx.dispatched
401 if c and c.index then
402 local tpl = require "luci.template"
404 if util.copcall(tpl.render, "indexer", {}) then
409 if type(target) == "function" then
410 util.copcall(function()
411 local oldenv = getfenv(target)
412 local module = require(c.module)
413 local env = setmetatable({}, {__index=
416 return rawget(tbl, key) or module[key] or oldenv[key]
423 if type(c.target) == "table" then
424 ok, err = util.copcall(target, c.target, unpack(args))
426 ok, err = util.copcall(target, unpack(args))
429 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
430 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
431 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
434 if not root or not root.target then
435 error404("No root node was registered, this usually happens if no module was installed.\n" ..
436 "Install luci-mod-admin-full and retry. " ..
437 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
439 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
440 "If this url belongs to an extension, make sure it is properly installed.\n" ..
441 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
446 --- Generate the dispatching index using the best possible strategy.
447 function createindex()
448 local path = luci.util.libpath() .. "/controller/"
449 local suff = { ".lua", ".lua.gz" }
451 if luci.util.copcall(require, "luci.fastindex") then
452 createindex_fastindex(path, suff)
454 createindex_plain(path, suff)
458 --- Generate the dispatching index using the fastindex C-indexer.
459 -- @param path Controller base directory
460 -- @param suffixes Controller file suffixes
461 function createindex_fastindex(path, suffixes)
465 fi = luci.fastindex.new("index")
466 for _, suffix in ipairs(suffixes) do
467 fi.add(path .. "*" .. suffix)
468 fi.add(path .. "*/*" .. suffix)
473 for k, v in pairs(fi.indexes) do
478 --- Generate the dispatching index using the native file-cache based strategy.
479 -- @param path Controller base directory
480 -- @param suffixes Controller file suffixes
481 function createindex_plain(path, suffixes)
482 local controllers = { }
483 for _, suffix in ipairs(suffixes) do
484 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
485 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
489 local cachedate = fs.stat(indexcache, "mtime")
492 for _, obj in ipairs(controllers) do
493 local omtime = fs.stat(obj, "mtime")
494 realdate = (omtime and omtime > realdate) and omtime or realdate
497 if cachedate > realdate then
499 sys.process.info("uid") == fs.stat(indexcache, "uid")
500 and fs.stat(indexcache, "modestr") == "rw-------",
501 "Fatal: Indexcache is not sane!"
504 index = loadfile(indexcache)()
512 for i,c in ipairs(controllers) do
513 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
514 for _, suffix in ipairs(suffixes) do
515 modname = modname:gsub(suffix.."$", "")
518 local mod = require(modname)
520 "Invalid controller file found\n" ..
521 "The file '" .. c .. "' contains an invalid module line.\n" ..
522 "Please verify whether the module name is set to '" .. modname ..
523 "' - It must correspond to the file path!")
525 local idx = mod.index
526 assert(type(idx) == "function",
527 "Invalid controller file found\n" ..
528 "The file '" .. c .. "' contains no index() function.\n" ..
529 "Please make sure that the controller contains a valid " ..
530 "index function and verify the spelling!")
536 local f = nixio.open(indexcache, "w", 600)
537 f:writeall(util.get_bytecode(index))
542 --- Create the dispatching tree from the index.
543 -- Build the index before if it does not exist yet.
544 function createtree()
550 local tree = {nodes={}, inreq=true}
553 ctx.treecache = setmetatable({}, {__mode="v"})
557 -- Load default translation
558 require "luci.i18n".loadc("base")
560 local scope = setmetatable({}, {__index = luci.dispatcher})
562 for k, v in pairs(index) do
568 local function modisort(a,b)
569 return modi[a].order < modi[b].order
572 for _, v in util.spairs(modi, modisort) do
573 scope._NAME = v.module
574 setfenv(v.func, scope)
581 --- Register a tree modifier.
582 -- @param func Modifier function
583 -- @param order Modifier order value (optional)
584 function modifier(func, order)
585 context.modifiers[#context.modifiers+1] = {
593 --- Clone a node of the dispatching tree to another position.
594 -- @param path Virtual path destination
595 -- @param clone Virtual path source
596 -- @param title Destination node title (optional)
597 -- @param order Destination node order value (optional)
598 -- @return Dispatching tree node
599 function assign(path, clone, title, order)
600 local obj = node(unpack(path))
607 setmetatable(obj, {__index = _create_node(clone)})
612 --- Create a new dispatching node and define common parameters.
613 -- @param path Virtual path
614 -- @param target Target function to call when dispatched.
615 -- @param title Destination node title
616 -- @param order Destination node order value (optional)
617 -- @return Dispatching tree node
618 function entry(path, target, title, order)
619 local c = node(unpack(path))
624 c.module = getfenv(2)._NAME
629 --- Fetch or create a dispatching node without setting the target module or
630 -- enabling the node.
631 -- @param ... Virtual path
632 -- @return Dispatching tree node
634 return _create_node({...})
637 --- Fetch or create a new dispatching node.
638 -- @param ... Virtual path
639 -- @return Dispatching tree node
641 local c = _create_node({...})
643 c.module = getfenv(2)._NAME
649 function _create_node(path)
654 local name = table.concat(path, ".")
655 local c = context.treecache[name]
658 local last = table.remove(path)
659 local parent = _create_node(path)
661 c = {nodes={}, auto=true}
662 -- the node is "in request" if the request path matches
663 -- at least up to the length of the node path
664 if parent.inreq and context.path[#path+1] == last then
667 parent.nodes[last] = c
668 context.treecache[name] = c
675 function _firstchild()
676 local path = { unpack(context.path) }
677 local name = table.concat(path, ".")
678 local node = context.treecache[name]
681 if node and node.nodes and next(node.nodes) then
683 for k, v in pairs(node.nodes) do
685 (v.order or 100) < (node.nodes[lowest].order or 100)
692 assert(lowest ~= nil,
693 "The requested node contains no childs, unable to redispatch")
695 path[#path+1] = lowest
699 --- Alias the first (lowest order) page automatically
700 function firstchild()
701 return { type = "firstchild", target = _firstchild }
704 --- Create a redirect to another dispatching node.
705 -- @param ... Virtual path destination
709 for _, r in ipairs({...}) do
717 --- Rewrite the first x path values of the request.
718 -- @param n Number of path values to replace
719 -- @param ... Virtual path to replace removed path values with
720 function rewrite(n, ...)
723 local dispatched = util.clone(context.dispatched)
726 table.remove(dispatched, 1)
729 for i, r in ipairs(req) do
730 table.insert(dispatched, i, r)
733 for _, r in ipairs({...}) do
734 dispatched[#dispatched+1] = r
742 local function _call(self, ...)
743 if #self.argv > 0 then
744 return getfenv()[self.name](unpack(self.argv), ...)
746 return getfenv()[self.name](...)
750 --- Create a function-call dispatching target.
751 -- @param name Target function of local controller
752 -- @param ... Additional parameters passed to the function
753 function call(name, ...)
754 return {type = "call", argv = {...}, name = name, target = _call}
758 local _template = function(self, ...)
759 require "luci.template".render(self.view)
762 --- Create a template render dispatching target.
763 -- @param name Template to be rendered
764 function template(name)
765 return {type = "template", view = name, target = _template}
769 local function _cbi(self, ...)
770 local cbi = require "luci.cbi"
771 local tpl = require "luci.template"
772 local http = require "luci.http"
774 local config = self.config or {}
775 local maps = cbi.load(self.model, ...)
779 for i, res in ipairs(maps) do
781 local cstate = res:parse()
782 if cstate and (not state or cstate < state) then
787 local function _resolve_path(path)
788 return type(path) == "table" and build_url(unpack(path)) or path
791 if config.on_valid_to and state and state > 0 and state < 2 then
792 http.redirect(_resolve_path(config.on_valid_to))
796 if config.on_changed_to and state and state > 1 then
797 http.redirect(_resolve_path(config.on_changed_to))
801 if config.on_success_to and state and state > 0 then
802 http.redirect(_resolve_path(config.on_success_to))
806 if config.state_handler then
807 if not config.state_handler(state, maps) then
812 http.header("X-CBI-State", state or 0)
814 if not config.noheader then
815 tpl.render("cbi/header", {state = state})
820 local applymap = false
821 local pageaction = true
822 local parsechain = { }
824 for i, res in ipairs(maps) do
825 if res.apply_needed and res.parsechain then
827 for _, c in ipairs(res.parsechain) do
828 parsechain[#parsechain+1] = c
834 redirect = redirect or res.redirect
837 if res.pageaction == false then
842 messages = messages or { }
843 messages[#messages+1] = res.message
847 for i, res in ipairs(maps) do
853 pageaction = pageaction,
854 parsechain = parsechain
858 if not config.nofooter then
859 tpl.render("cbi/footer", {
861 pageaction = pageaction,
864 autoapply = config.autoapply
869 --- Create a CBI model dispatching target.
870 -- @param model CBI model to be rendered
871 function cbi(model, config)
872 return {type = "cbi", config = config, model = model, target = _cbi}
876 local function _arcombine(self, ...)
878 local target = #argv > 0 and self.targets[2] or self.targets[1]
879 setfenv(target.target, self.env)
880 target:target(unpack(argv))
883 --- Create a combined dispatching target for non argv and argv requests.
884 -- @param trg1 Overview Target
885 -- @param trg2 Detail Target
886 function arcombine(trg1, trg2)
887 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
891 local function _form(self, ...)
892 local cbi = require "luci.cbi"
893 local tpl = require "luci.template"
894 local http = require "luci.http"
896 local maps = luci.cbi.load(self.model, ...)
899 for i, res in ipairs(maps) do
900 local cstate = res:parse()
901 if cstate and (not state or cstate < state) then
906 http.header("X-CBI-State", state or 0)
908 for i, res in ipairs(maps) do
914 --- Create a CBI form model dispatching target.
915 -- @param model CBI form model tpo be rendered
917 return {type = "cbi", model = model, target = _form}
920 --- Access the luci.i18n translate() api.
923 -- @param text Text to translate
924 translate = i18n.translate
926 --- No-op function used to mark translation entries for menu labels.
927 -- This function does not actually translate the given argument but
928 -- is used by build/i18n-scan.pl to find translatable entries.