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()
47 --- Build the URL relative to the server webroot from given virtual path.
48 -- @param ... Virtual path
49 -- @return Relative URL
50 function build_url(...)
52 local sn = http.getenv("SCRIPT_NAME") or ""
53 for k, v in pairs(context.urltoken) do
54 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
56 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
59 --- Send a 404 error code and render the "error404" template if available.
60 -- @param message Custom error message (optional)
62 function error404(message)
63 luci.http.status(404, "Not Found")
64 message = message or "Not Found"
66 require("luci.template")
67 if not luci.util.copcall(luci.template.render, "error404") then
68 luci.http.prepare_content("text/plain")
69 luci.http.write(message)
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message Custom error message (optional)#
77 function error500(message)
78 luci.util.perror(message)
79 if not context.template_header_sent then
80 luci.http.status(500, "Internal Server Error")
81 luci.http.prepare_content("text/plain")
82 luci.http.write(message)
84 require("luci.template")
85 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
86 luci.http.prepare_content("text/plain")
87 luci.http.write(message)
93 function authenticator.htmlauth(validator, accs, default)
94 local user = luci.http.formvalue("username")
95 local pass = luci.http.formvalue("password")
97 if user and validator(user, pass) then
102 require("luci.template")
104 luci.template.render("sysauth", {duser=default, fuser=user})
109 --- Dispatch an HTTP request.
110 -- @param request LuCI HTTP Request object
111 function httpdispatch(request)
112 luci.http.context.request = request
114 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
116 for node in pathinfo:gmatch("[^/]+") do
117 table.insert(context.request, node)
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
126 --context._disable_memtrace()
129 --- Dispatches a LuCI virtual path.
130 -- @param request Virtual path
131 function dispatch(request)
132 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
135 ctx.urltoken = ctx.urltoken or {}
137 local conf = require "luci.config"
139 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
141 local lang = conf.main.lang
142 if lang == "auto" then
143 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
144 for lpat in aclang:gmatch("[%w-]+") do
145 lpat = lpat and lpat:gsub("-", "_")
146 if conf.languages[lpat] then
152 require "luci.i18n".setlanguage(lang)
163 ctx.requestargs = ctx.requestargs or args
166 local token = ctx.urltoken
170 for i, s in ipairs(request) do
173 tkey, tval = s:match(";(%w+)=(.*)")
188 util.update(track, c)
197 for j=n+1, #request do
198 args[#args+1] = request[j]
199 freq[#freq+1] = request[j]
203 ctx.requestpath = freq
207 require("luci.i18n").loadc(track.i18n)
210 -- Init template engine
211 if (c and c.index) or not track.notemplate then
212 local tpl = require("luci.template")
213 local media = track.mediaurlbase or luci.config.main.mediaurlbase
214 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
215 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
217 for name, theme in pairs(luci.config.themes) do
218 if name:sub(1,1) ~= "." and pcall(tpl.Template,
219 "themes/%s/header" % fs.basename(theme)) then
223 assert(media, "No valid theme found")
226 tpl.context.viewns = setmetatable({
227 write = luci.http.write;
228 include = function(name) tpl.Template(name):render(getfenv(2)) end;
229 translate = function(...) return require("luci.i18n").translate(...) end;
230 striptags = util.striptags;
232 theme = fs.basename(media);
233 resource = luci.config.main.resourcebase
234 }, {__index=function(table, key)
235 if key == "controller" then
237 elseif key == "REQUEST_URI" then
238 return build_url(unpack(ctx.requestpath))
240 return rawget(table, key) or _G[key]
245 track.dependent = (track.dependent ~= false)
246 assert(not track.dependent or not track.auto, "Access Violation")
248 if track.sysauth then
249 local sauth = require "luci.sauth"
251 local authen = type(track.sysauth_authenticator) == "function"
252 and track.sysauth_authenticator
253 or authenticator[track.sysauth_authenticator]
255 local def = (type(track.sysauth) == "string") and track.sysauth
256 local accs = def and {track.sysauth} or track.sysauth
257 local sess = ctx.authsession
258 local verifytoken = false
260 sess = luci.http.getcookie("sysauth")
261 sess = sess and sess:match("^[a-f0-9]*$")
265 local sdat = sauth.read(sess)
269 sdat = loadstring(sdat)
272 if not verifytoken or ctx.urltoken.stok == sdat.token then
276 local eu = http.getenv("HTTP_AUTH_USER")
277 local ep = http.getenv("HTTP_AUTH_PASS")
278 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
279 authen = function() return eu end
283 if not util.contains(accs, user) then
285 ctx.urltoken.stok = nil
286 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
287 if not user or not util.contains(accs, user) then
290 local sid = sess or luci.sys.uniqueid(16)
292 local token = luci.sys.uniqueid(16)
293 sauth.write(sid, util.get_bytecode({
296 secret=luci.sys.uniqueid(16)
298 ctx.urltoken.stok = token
300 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
301 ctx.authsession = sid
304 luci.http.status(403, "Forbidden")
308 ctx.authsession = sess
312 if track.setgroup then
313 luci.sys.process.setgroup(track.setgroup)
316 if track.setuser then
317 luci.sys.process.setuser(track.setuser)
322 if type(c.target) == "function" then
324 elseif type(c.target) == "table" then
325 target = c.target.target
329 if c and (c.index or type(target) == "function") then
331 ctx.requested = ctx.requested or ctx.dispatched
334 if c and c.index then
335 local tpl = require "luci.template"
337 if util.copcall(tpl.render, "indexer", {}) then
342 if type(target) == "function" then
343 util.copcall(function()
344 local oldenv = getfenv(target)
345 local module = require(c.module)
346 local env = setmetatable({}, {__index=
349 return rawget(tbl, key) or module[key] or oldenv[key]
355 if type(c.target) == "table" then
356 target(c.target, unpack(args))
365 --- Generate the dispatching index using the best possible strategy.
366 function createindex()
367 local path = luci.util.libpath() .. "/controller/"
368 local suff = { ".lua", ".lua.gz" }
370 if luci.util.copcall(require, "luci.fastindex") then
371 createindex_fastindex(path, suff)
373 createindex_plain(path, suff)
377 --- Generate the dispatching index using the fastindex C-indexer.
378 -- @param path Controller base directory
379 -- @param suffixes Controller file suffixes
380 function createindex_fastindex(path, suffixes)
384 fi = luci.fastindex.new("index")
385 for _, suffix in ipairs(suffixes) do
386 fi.add(path .. "*" .. suffix)
387 fi.add(path .. "*/*" .. suffix)
392 for k, v in pairs(fi.indexes) do
397 --- Generate the dispatching index using the native file-cache based strategy.
398 -- @param path Controller base directory
399 -- @param suffixes Controller file suffixes
400 function createindex_plain(path, suffixes)
401 local controllers = { }
402 for _, suffix in ipairs(suffixes) do
403 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
404 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
408 local cachedate = fs.stat(indexcache, "mtime")
411 for _, obj in ipairs(controllers) do
412 local omtime = fs.stat(path .. "/" .. obj, "mtime")
413 realdate = (omtime and omtime > realdate) and omtime or realdate
416 if cachedate > realdate then
418 sys.process.info("uid") == fs.stat(indexcache, "uid")
419 and fs.stat(indexcache, "modestr") == "rw-------",
420 "Fatal: Indexcache is not sane!"
423 index = loadfile(indexcache)()
431 for i,c in ipairs(controllers) do
432 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
433 for _, suffix in ipairs(suffixes) do
434 module = module:gsub(suffix.."$", "")
437 local mod = require(module)
438 local idx = mod.index
440 if type(idx) == "function" then
446 local f = nixio.open(indexcache, "w", 600)
447 f:writeall(util.get_bytecode(index))
452 --- Create the dispatching tree from the index.
453 -- Build the index before if it does not exist yet.
454 function createtree()
460 local tree = {nodes={}}
463 ctx.treecache = setmetatable({}, {__mode="v"})
467 -- Load default translation
468 require "luci.i18n".loadc("default")
470 local scope = setmetatable({}, {__index = luci.dispatcher})
472 for k, v in pairs(index) do
478 local function modisort(a,b)
479 return modi[a].order < modi[b].order
482 for _, v in util.spairs(modi, modisort) do
483 scope._NAME = v.module
484 setfenv(v.func, scope)
491 --- Register a tree modifier.
492 -- @param func Modifier function
493 -- @param order Modifier order value (optional)
494 function modifier(func, order)
495 context.modifiers[#context.modifiers+1] = {
503 --- Clone a node of the dispatching tree to another position.
504 -- @param path Virtual path destination
505 -- @param clone Virtual path source
506 -- @param title Destination node title (optional)
507 -- @param order Destination node order value (optional)
508 -- @return Dispatching tree node
509 function assign(path, clone, title, order)
510 local obj = node(unpack(path))
517 setmetatable(obj, {__index = _create_node(clone)})
522 --- Create a new dispatching node and define common parameters.
523 -- @param path Virtual path
524 -- @param target Target function to call when dispatched.
525 -- @param title Destination node title
526 -- @param order Destination node order value (optional)
527 -- @return Dispatching tree node
528 function entry(path, target, title, order)
529 local c = node(unpack(path))
534 c.module = getfenv(2)._NAME
539 --- Fetch or create a dispatching node without setting the target module or
540 -- enabling the node.
541 -- @param ... Virtual path
542 -- @return Dispatching tree node
544 return _create_node({...})
547 --- Fetch or create a new dispatching node.
548 -- @param ... Virtual path
549 -- @return Dispatching tree node
551 local c = _create_node({...})
553 c.module = getfenv(2)._NAME
559 function _create_node(path, cache)
564 cache = cache or context.treecache
565 local name = table.concat(path, ".")
566 local c = cache[name]
569 local new = {nodes={}, auto=true, path=util.clone(path)}
570 local last = table.remove(path)
572 c = _create_node(path, cache)
585 --- Create a redirect to another dispatching node.
586 -- @param ... Virtual path destination
590 for _, r in ipairs({...}) do
598 --- Rewrite the first x path values of the request.
599 -- @param n Number of path values to replace
600 -- @param ... Virtual path to replace removed path values with
601 function rewrite(n, ...)
604 local dispatched = util.clone(context.dispatched)
607 table.remove(dispatched, 1)
610 for i, r in ipairs(req) do
611 table.insert(dispatched, i, r)
614 for _, r in ipairs({...}) do
615 dispatched[#dispatched+1] = r
623 local function _call(self, ...)
624 if #self.argv > 0 then
625 return getfenv()[self.name](unpack(self.argv), ...)
627 return getfenv()[self.name](...)
631 --- Create a function-call dispatching target.
632 -- @param name Target function of local controller
633 -- @param ... Additional parameters passed to the function
634 function call(name, ...)
635 return {type = "call", argv = {...}, name = name, target = _call}
639 local _template = function(self, ...)
640 require "luci.template".render(self.view)
643 --- Create a template render dispatching target.
644 -- @param name Template to be rendered
645 function template(name)
646 return {type = "template", view = name, target = _template}
650 local function _cbi(self, ...)
651 local cbi = require "luci.cbi"
652 local tpl = require "luci.template"
653 local http = require "luci.http"
655 local config = self.config or {}
656 local maps = cbi.load(self.model, ...)
660 for i, res in ipairs(maps) do
662 local cstate = res:parse()
663 if cstate and (not state or cstate < state) then
668 local function _resolve_path(path)
669 return type(path) == "table" and build_url(unpack(path)) or path
672 if config.on_valid_to and state and state > 0 and state < 2 then
673 http.redirect(_resolve_path(config.on_valid_to))
677 if config.on_changed_to and state and state > 1 then
678 http.redirect(_resolve_path(config.on_changed_to))
682 if config.on_success_to and state and state > 0 then
683 http.redirect(_resolve_path(config.on_success_to))
687 if config.state_handler then
688 if not config.state_handler(state, maps) then
693 local pageaction = true
694 http.header("X-CBI-State", state or 0)
695 if not config.noheader then
696 tpl.render("cbi/header", {state = state})
698 for i, res in ipairs(maps) do
700 if res.pageaction == false then
704 if not config.nofooter then
705 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
709 --- Create a CBI model dispatching target.
710 -- @param model CBI model to be rendered
711 function cbi(model, config)
712 return {type = "cbi", config = config, model = model, target = _cbi}
716 local function _arcombine(self, ...)
718 local target = #argv > 0 and self.targets[2] or self.targets[1]
719 setfenv(target.target, self.env)
720 target:target(unpack(argv))
723 --- Create a combined dispatching target for non argv and argv requests.
724 -- @param trg1 Overview Target
725 -- @param trg2 Detail Target
726 function arcombine(trg1, trg2)
727 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
731 local function _form(self, ...)
732 local cbi = require "luci.cbi"
733 local tpl = require "luci.template"
734 local http = require "luci.http"
736 local maps = luci.cbi.load(self.model, ...)
739 for i, res in ipairs(maps) do
740 local cstate = res:parse()
741 if cstate and (not state or cstate < state) then
746 http.header("X-CBI-State", state or 0)
748 for i, res in ipairs(maps) do
754 --- Create a CBI form model dispatching target.
755 -- @param model CBI form model tpo be rendered
757 return {type = "cbi", model = model, target = _form}