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 --- Send a 404 error code and render the "error404" template if available.
77 -- @param message Custom error message (optional)
79 function error404(message)
80 luci.http.status(404, "Not Found")
81 message = message or "Not Found"
83 require("luci.template")
84 if not luci.util.copcall(luci.template.render, "error404") then
85 luci.http.prepare_content("text/plain")
86 luci.http.write(message)
91 --- Send a 500 error code and render the "error500" template if available.
92 -- @param message Custom error message (optional)#
94 function error500(message)
95 luci.util.perror(message)
96 if not context.template_header_sent then
97 luci.http.status(500, "Internal Server Error")
98 luci.http.prepare_content("text/plain")
99 luci.http.write(message)
101 require("luci.template")
102 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
103 luci.http.prepare_content("text/plain")
104 luci.http.write(message)
110 function authenticator.htmlauth(validator, accs, default)
111 local user = luci.http.formvalue("username")
112 local pass = luci.http.formvalue("password")
114 if user and validator(user, pass) then
119 require("luci.template")
121 luci.template.render("sysauth", {duser=default, fuser=user})
126 --- Dispatch an HTTP request.
127 -- @param request LuCI HTTP Request object
128 function httpdispatch(request, prefix)
129 luci.http.context.request = request
133 context.urltoken = {}
135 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
138 for _, node in ipairs(prefix) do
143 local tokensok = true
144 for node in pathinfo:gmatch("[^/]+") do
147 tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)")
150 context.urltoken[tkey] = tval
157 local stat, err = util.coxpcall(function()
158 dispatch(context.request)
163 --context._disable_memtrace()
166 --- Dispatches a LuCI virtual path.
167 -- @param request Virtual path
168 function dispatch(request)
169 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
173 local conf = require "luci.config"
175 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
177 local lang = conf.main.lang or "auto"
178 if lang == "auto" then
179 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
180 for lpat in aclang:gmatch("[%w-]+") do
181 lpat = lpat and lpat:gsub("-", "_")
182 if conf.languages[lpat] then
188 require "luci.i18n".setlanguage(lang)
199 ctx.requestargs = ctx.requestargs or args
201 local token = ctx.urltoken
205 for i, s in ipairs(request) do
214 util.update(track, c)
222 for j=n+1, #request do
223 args[#args+1] = request[j]
224 freq[#freq+1] = request[j]
228 ctx.requestpath = ctx.requestpath or freq
232 i18n.loadc(track.i18n)
235 -- Init template engine
236 if (c and c.index) or not track.notemplate then
237 local tpl = require("luci.template")
238 local media = track.mediaurlbase or luci.config.main.mediaurlbase
239 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
241 for name, theme in pairs(luci.config.themes) do
242 if name:sub(1,1) ~= "." and pcall(tpl.Template,
243 "themes/%s/header" % fs.basename(theme)) then
247 assert(media, "No valid theme found")
250 tpl.context.viewns = setmetatable({
251 write = luci.http.write;
252 include = function(name) tpl.Template(name):render(getfenv(2)) end;
253 translate = i18n.translate;
254 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
255 striptags = util.striptags;
256 pcdata = util.pcdata;
258 theme = fs.basename(media);
259 resource = luci.config.main.resourcebase
260 }, {__index=function(table, key)
261 if key == "controller" then
263 elseif key == "REQUEST_URI" then
264 return build_url(unpack(ctx.requestpath))
266 return rawget(table, key) or _G[key]
271 track.dependent = (track.dependent ~= false)
272 assert(not track.dependent or not track.auto,
273 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
274 "has no parent node so the access to this location has been denied.\n" ..
275 "This is a software bug, please report this message at " ..
276 "http://luci.subsignal.org/trac/newticket"
279 if track.sysauth then
280 local sauth = require "luci.sauth"
282 local authen = type(track.sysauth_authenticator) == "function"
283 and track.sysauth_authenticator
284 or authenticator[track.sysauth_authenticator]
286 local def = (type(track.sysauth) == "string") and track.sysauth
287 local accs = def and {track.sysauth} or track.sysauth
288 local sess = ctx.authsession
289 local verifytoken = false
291 sess = luci.http.getcookie("sysauth")
292 sess = sess and sess:match("^[a-f0-9]*$")
296 local sdat = sauth.read(sess)
300 sdat = loadstring(sdat)
303 if not verifytoken or ctx.urltoken.stok == sdat.token then
307 local eu = http.getenv("HTTP_AUTH_USER")
308 local ep = http.getenv("HTTP_AUTH_PASS")
309 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
310 authen = function() return eu end
314 if not util.contains(accs, user) then
316 ctx.urltoken.stok = nil
317 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
318 if not user or not util.contains(accs, user) then
321 local sid = sess or luci.sys.uniqueid(16)
323 local token = luci.sys.uniqueid(16)
324 sauth.write(sid, util.get_bytecode({
327 secret=luci.sys.uniqueid(16)
329 ctx.urltoken.stok = token
331 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
332 ctx.authsession = sid
336 luci.http.status(403, "Forbidden")
340 ctx.authsession = sess
345 if track.setgroup then
346 luci.sys.process.setgroup(track.setgroup)
349 if track.setuser then
350 luci.sys.process.setuser(track.setuser)
355 if type(c.target) == "function" then
357 elseif type(c.target) == "table" then
358 target = c.target.target
362 if c and (c.index or type(target) == "function") then
364 ctx.requested = ctx.requested or ctx.dispatched
367 if c and c.index then
368 local tpl = require "luci.template"
370 if util.copcall(tpl.render, "indexer", {}) then
375 if type(target) == "function" then
376 util.copcall(function()
377 local oldenv = getfenv(target)
378 local module = require(c.module)
379 local env = setmetatable({}, {__index=
382 return rawget(tbl, key) or module[key] or oldenv[key]
389 if type(c.target) == "table" then
390 ok, err = util.copcall(target, c.target, unpack(args))
392 ok, err = util.copcall(target, unpack(args))
395 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
396 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
397 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
400 if not root or not root.target then
401 error404("No root node was registered, this usually happens if no module was installed.\n" ..
402 "Install luci-mod-admin-full and retry. " ..
403 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
405 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
406 "If this url belongs to an extension, make sure it is properly installed.\n" ..
407 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
412 --- Generate the dispatching index using the best possible strategy.
413 function createindex()
414 local path = luci.util.libpath() .. "/controller/"
415 local suff = { ".lua", ".lua.gz" }
417 if luci.util.copcall(require, "luci.fastindex") then
418 createindex_fastindex(path, suff)
420 createindex_plain(path, suff)
424 --- Generate the dispatching index using the fastindex C-indexer.
425 -- @param path Controller base directory
426 -- @param suffixes Controller file suffixes
427 function createindex_fastindex(path, suffixes)
431 fi = luci.fastindex.new("index")
432 for _, suffix in ipairs(suffixes) do
433 fi.add(path .. "*" .. suffix)
434 fi.add(path .. "*/*" .. suffix)
439 for k, v in pairs(fi.indexes) do
444 --- Generate the dispatching index using the native file-cache based strategy.
445 -- @param path Controller base directory
446 -- @param suffixes Controller file suffixes
447 function createindex_plain(path, suffixes)
448 local controllers = { }
449 for _, suffix in ipairs(suffixes) do
450 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
451 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
455 local cachedate = fs.stat(indexcache, "mtime")
458 for _, obj in ipairs(controllers) do
459 local omtime = fs.stat(obj, "mtime")
460 realdate = (omtime and omtime > realdate) and omtime or realdate
463 if cachedate > realdate then
465 sys.process.info("uid") == fs.stat(indexcache, "uid")
466 and fs.stat(indexcache, "modestr") == "rw-------",
467 "Fatal: Indexcache is not sane!"
470 index = loadfile(indexcache)()
478 for i,c in ipairs(controllers) do
479 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
480 for _, suffix in ipairs(suffixes) do
481 modname = modname:gsub(suffix.."$", "")
484 local mod = require(modname)
486 "Invalid controller file found\n" ..
487 "The file '" .. c .. "' contains an invalid module line.\n" ..
488 "Please verify whether the module name is set to '" .. modname ..
489 "' - It must correspond to the file path!")
491 local idx = mod.index
492 assert(type(idx) == "function",
493 "Invalid controller file found\n" ..
494 "The file '" .. c .. "' contains no index() function.\n" ..
495 "Please make sure that the controller contains a valid " ..
496 "index function and verify the spelling!")
502 local f = nixio.open(indexcache, "w", 600)
503 f:writeall(util.get_bytecode(index))
508 --- Create the dispatching tree from the index.
509 -- Build the index before if it does not exist yet.
510 function createtree()
516 local tree = {nodes={}, inreq=true}
519 ctx.treecache = setmetatable({}, {__mode="v"})
523 -- Load default translation
524 require "luci.i18n".loadc("base")
526 local scope = setmetatable({}, {__index = luci.dispatcher})
528 for k, v in pairs(index) do
534 local function modisort(a,b)
535 return modi[a].order < modi[b].order
538 for _, v in util.spairs(modi, modisort) do
539 scope._NAME = v.module
540 setfenv(v.func, scope)
547 --- Register a tree modifier.
548 -- @param func Modifier function
549 -- @param order Modifier order value (optional)
550 function modifier(func, order)
551 context.modifiers[#context.modifiers+1] = {
559 --- Clone a node of the dispatching tree to another position.
560 -- @param path Virtual path destination
561 -- @param clone Virtual path source
562 -- @param title Destination node title (optional)
563 -- @param order Destination node order value (optional)
564 -- @return Dispatching tree node
565 function assign(path, clone, title, order)
566 local obj = node(unpack(path))
573 setmetatable(obj, {__index = _create_node(clone)})
578 --- Create a new dispatching node and define common parameters.
579 -- @param path Virtual path
580 -- @param target Target function to call when dispatched.
581 -- @param title Destination node title
582 -- @param order Destination node order value (optional)
583 -- @return Dispatching tree node
584 function entry(path, target, title, order)
585 local c = node(unpack(path))
590 c.module = getfenv(2)._NAME
595 --- Fetch or create a dispatching node without setting the target module or
596 -- enabling the node.
597 -- @param ... Virtual path
598 -- @return Dispatching tree node
600 return _create_node({...})
603 --- Fetch or create a new dispatching node.
604 -- @param ... Virtual path
605 -- @return Dispatching tree node
607 local c = _create_node({...})
609 c.module = getfenv(2)._NAME
615 function _create_node(path)
620 local name = table.concat(path, ".")
621 local c = context.treecache[name]
624 local last = table.remove(path)
625 local parent = _create_node(path)
627 c = {nodes={}, auto=true}
628 -- the node is "in request" if the request path matches
629 -- at least up to the length of the node path
630 if parent.inreq and context.path[#path+1] == last then
633 parent.nodes[last] = c
634 context.treecache[name] = c
641 function _firstchild()
642 local path = { unpack(context.path) }
643 local name = table.concat(path, ".")
644 local node = context.treecache[name]
647 if node and node.nodes and next(node.nodes) then
649 for k, v in pairs(node.nodes) do
651 (v.order or 100) < (node.nodes[lowest].order or 100)
658 assert(lowest ~= nil,
659 "The requested node contains no childs, unable to redispatch")
661 path[#path+1] = lowest
665 --- Alias the first (lowest order) page automatically
666 function firstchild()
667 return { type = "firstchild", target = _firstchild }
670 --- Create a redirect to another dispatching node.
671 -- @param ... Virtual path destination
675 for _, r in ipairs({...}) do
683 --- Rewrite the first x path values of the request.
684 -- @param n Number of path values to replace
685 -- @param ... Virtual path to replace removed path values with
686 function rewrite(n, ...)
689 local dispatched = util.clone(context.dispatched)
692 table.remove(dispatched, 1)
695 for i, r in ipairs(req) do
696 table.insert(dispatched, i, r)
699 for _, r in ipairs({...}) do
700 dispatched[#dispatched+1] = r
708 local function _call(self, ...)
709 if #self.argv > 0 then
710 return getfenv()[self.name](unpack(self.argv), ...)
712 return getfenv()[self.name](...)
716 --- Create a function-call dispatching target.
717 -- @param name Target function of local controller
718 -- @param ... Additional parameters passed to the function
719 function call(name, ...)
720 return {type = "call", argv = {...}, name = name, target = _call}
724 local _template = function(self, ...)
725 require "luci.template".render(self.view)
728 --- Create a template render dispatching target.
729 -- @param name Template to be rendered
730 function template(name)
731 return {type = "template", view = name, target = _template}
735 local function _cbi(self, ...)
736 local cbi = require "luci.cbi"
737 local tpl = require "luci.template"
738 local http = require "luci.http"
740 local config = self.config or {}
741 local maps = cbi.load(self.model, ...)
745 for i, res in ipairs(maps) do
747 local cstate = res:parse()
748 if cstate and (not state or cstate < state) then
753 local function _resolve_path(path)
754 return type(path) == "table" and build_url(unpack(path)) or path
757 if config.on_valid_to and state and state > 0 and state < 2 then
758 http.redirect(_resolve_path(config.on_valid_to))
762 if config.on_changed_to and state and state > 1 then
763 http.redirect(_resolve_path(config.on_changed_to))
767 if config.on_success_to and state and state > 0 then
768 http.redirect(_resolve_path(config.on_success_to))
772 if config.state_handler then
773 if not config.state_handler(state, maps) then
778 http.header("X-CBI-State", state or 0)
780 if not config.noheader then
781 tpl.render("cbi/header", {state = state})
786 local applymap = false
787 local pageaction = true
788 local parsechain = { }
790 for i, res in ipairs(maps) do
791 if res.apply_needed and res.parsechain then
793 for _, c in ipairs(res.parsechain) do
794 parsechain[#parsechain+1] = c
800 redirect = redirect or res.redirect
803 if res.pageaction == false then
808 messages = messages or { }
809 messages[#messages+1] = res.message
813 for i, res in ipairs(maps) do
819 pageaction = pageaction,
820 parsechain = parsechain
824 if not config.nofooter then
825 tpl.render("cbi/footer", {
827 pageaction = pageaction,
830 autoapply = config.autoapply
835 --- Create a CBI model dispatching target.
836 -- @param model CBI model to be rendered
837 function cbi(model, config)
838 return {type = "cbi", config = config, model = model, target = _cbi}
842 local function _arcombine(self, ...)
844 local target = #argv > 0 and self.targets[2] or self.targets[1]
845 setfenv(target.target, self.env)
846 target:target(unpack(argv))
849 --- Create a combined dispatching target for non argv and argv requests.
850 -- @param trg1 Overview Target
851 -- @param trg2 Detail Target
852 function arcombine(trg1, trg2)
853 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
857 local function _form(self, ...)
858 local cbi = require "luci.cbi"
859 local tpl = require "luci.template"
860 local http = require "luci.http"
862 local maps = luci.cbi.load(self.model, ...)
865 for i, res in ipairs(maps) do
866 local cstate = res:parse()
867 if cstate and (not state or cstate < state) then
872 http.header("X-CBI-State", state or 0)
874 for i, res in ipairs(maps) do
880 --- Create a CBI form model dispatching target.
881 -- @param model CBI form model tpo be rendered
883 return {type = "cbi", model = model, target = _form}
886 --- Access the luci.i18n translate() api.
889 -- @param text Text to translate
890 translate = i18n.translate
892 --- No-op function used to mark translation entries for menu labels.
893 -- This function does not actually translate the given argument but
894 -- is used by build/i18n-scan.pl to find translatable entries.