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 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
136 for _, node in ipairs(prefix) do
141 for node in pathinfo:gmatch("[^/]+") do
145 local stat, err = util.coxpcall(function()
146 dispatch(context.request)
151 --context._disable_memtrace()
154 --- Dispatches a LuCI virtual path.
155 -- @param request Virtual path
156 function dispatch(request)
157 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
160 ctx.urltoken = ctx.urltoken or {}
162 local conf = require "luci.config"
164 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
166 local lang = conf.main.lang or "auto"
167 if lang == "auto" then
168 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
169 for lpat in aclang:gmatch("[%w-]+") do
170 lpat = lpat and lpat:gsub("-", "_")
171 if conf.languages[lpat] then
177 require "luci.i18n".setlanguage(lang)
188 ctx.requestargs = ctx.requestargs or args
191 local token = ctx.urltoken
195 for i, s in ipairs(request) do
198 tkey, tval = s:match(";(%w+)=([a-fA-F0-9]*)")
213 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 require("luci.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 = function(...) return require("luci.i18n").translate(...) end;
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, "Access Violation")
274 if track.sysauth then
275 local sauth = require "luci.sauth"
277 local authen = type(track.sysauth_authenticator) == "function"
278 and track.sysauth_authenticator
279 or authenticator[track.sysauth_authenticator]
281 local def = (type(track.sysauth) == "string") and track.sysauth
282 local accs = def and {track.sysauth} or track.sysauth
283 local sess = ctx.authsession
284 local verifytoken = false
286 sess = luci.http.getcookie("sysauth")
287 sess = sess and sess:match("^[a-f0-9]*$")
291 local sdat = sauth.read(sess)
295 sdat = loadstring(sdat)
298 if not verifytoken or ctx.urltoken.stok == sdat.token then
302 local eu = http.getenv("HTTP_AUTH_USER")
303 local ep = http.getenv("HTTP_AUTH_PASS")
304 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
305 authen = function() return eu end
309 if not util.contains(accs, user) then
311 ctx.urltoken.stok = nil
312 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
313 if not user or not util.contains(accs, user) then
316 local sid = sess or luci.sys.uniqueid(16)
318 local token = luci.sys.uniqueid(16)
319 sauth.write(sid, util.get_bytecode({
322 secret=luci.sys.uniqueid(16)
324 ctx.urltoken.stok = token
326 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
327 ctx.authsession = sid
331 luci.http.status(403, "Forbidden")
335 ctx.authsession = sess
340 if track.setgroup then
341 luci.sys.process.setgroup(track.setgroup)
344 if track.setuser then
345 luci.sys.process.setuser(track.setuser)
350 if type(c.target) == "function" then
352 elseif type(c.target) == "table" then
353 target = c.target.target
357 if c and (c.index or type(target) == "function") then
359 ctx.requested = ctx.requested or ctx.dispatched
362 if c and c.index then
363 local tpl = require "luci.template"
365 if util.copcall(tpl.render, "indexer", {}) then
370 if type(target) == "function" then
371 util.copcall(function()
372 local oldenv = getfenv(target)
373 local module = require(c.module)
374 local env = setmetatable({}, {__index=
377 return rawget(tbl, key) or module[key] or oldenv[key]
383 if type(c.target) == "table" then
384 target(c.target, unpack(args))
393 --- Generate the dispatching index using the best possible strategy.
394 function createindex()
395 local path = luci.util.libpath() .. "/controller/"
396 local suff = { ".lua", ".lua.gz" }
398 if luci.util.copcall(require, "luci.fastindex") then
399 createindex_fastindex(path, suff)
401 createindex_plain(path, suff)
405 --- Generate the dispatching index using the fastindex C-indexer.
406 -- @param path Controller base directory
407 -- @param suffixes Controller file suffixes
408 function createindex_fastindex(path, suffixes)
412 fi = luci.fastindex.new("index")
413 for _, suffix in ipairs(suffixes) do
414 fi.add(path .. "*" .. suffix)
415 fi.add(path .. "*/*" .. suffix)
420 for k, v in pairs(fi.indexes) do
425 --- Generate the dispatching index using the native file-cache based strategy.
426 -- @param path Controller base directory
427 -- @param suffixes Controller file suffixes
428 function createindex_plain(path, suffixes)
429 local controllers = { }
430 for _, suffix in ipairs(suffixes) do
431 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
432 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
436 local cachedate = fs.stat(indexcache, "mtime")
439 for _, obj in ipairs(controllers) do
440 local omtime = fs.stat(path .. "/" .. obj, "mtime")
441 realdate = (omtime and omtime > realdate) and omtime or realdate
444 if cachedate > realdate then
446 sys.process.info("uid") == fs.stat(indexcache, "uid")
447 and fs.stat(indexcache, "modestr") == "rw-------",
448 "Fatal: Indexcache is not sane!"
451 index = loadfile(indexcache)()
459 for i,c in ipairs(controllers) do
460 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
461 for _, suffix in ipairs(suffixes) do
462 modname = modname:gsub(suffix.."$", "")
465 local mod = require(modname)
466 local idx = mod.index
468 if type(idx) == "function" then
474 local f = nixio.open(indexcache, "w", 600)
475 f:writeall(util.get_bytecode(index))
480 --- Create the dispatching tree from the index.
481 -- Build the index before if it does not exist yet.
482 function createtree()
488 local tree = {nodes={}}
491 ctx.treecache = setmetatable({}, {__mode="v"})
495 -- Load default translation
496 require "luci.i18n".loadc("base")
498 local scope = setmetatable({}, {__index = luci.dispatcher})
500 for k, v in pairs(index) do
506 local function modisort(a,b)
507 return modi[a].order < modi[b].order
510 for _, v in util.spairs(modi, modisort) do
511 scope._NAME = v.module
512 setfenv(v.func, scope)
519 --- Register a tree modifier.
520 -- @param func Modifier function
521 -- @param order Modifier order value (optional)
522 function modifier(func, order)
523 context.modifiers[#context.modifiers+1] = {
531 --- Clone a node of the dispatching tree to another position.
532 -- @param path Virtual path destination
533 -- @param clone Virtual path source
534 -- @param title Destination node title (optional)
535 -- @param order Destination node order value (optional)
536 -- @return Dispatching tree node
537 function assign(path, clone, title, order)
538 local obj = node(unpack(path))
545 setmetatable(obj, {__index = _create_node(clone)})
550 --- Create a new dispatching node and define common parameters.
551 -- @param path Virtual path
552 -- @param target Target function to call when dispatched.
553 -- @param title Destination node title
554 -- @param order Destination node order value (optional)
555 -- @return Dispatching tree node
556 function entry(path, target, title, order)
557 local c = node(unpack(path))
562 c.module = getfenv(2)._NAME
567 --- Fetch or create a dispatching node without setting the target module or
568 -- enabling the node.
569 -- @param ... Virtual path
570 -- @return Dispatching tree node
572 return _create_node({...})
575 --- Fetch or create a new dispatching node.
576 -- @param ... Virtual path
577 -- @return Dispatching tree node
579 local c = _create_node({...})
581 c.module = getfenv(2)._NAME
587 function _create_node(path, cache)
592 cache = cache or context.treecache
593 local name = table.concat(path, ".")
594 local c = cache[name]
597 local new = {nodes={}, auto=true, path=util.clone(path)}
598 local last = table.remove(path)
600 c = _create_node(path, cache)
613 --- Create a redirect to another dispatching node.
614 -- @param ... Virtual path destination
618 for _, r in ipairs({...}) do
626 --- Rewrite the first x path values of the request.
627 -- @param n Number of path values to replace
628 -- @param ... Virtual path to replace removed path values with
629 function rewrite(n, ...)
632 local dispatched = util.clone(context.dispatched)
635 table.remove(dispatched, 1)
638 for i, r in ipairs(req) do
639 table.insert(dispatched, i, r)
642 for _, r in ipairs({...}) do
643 dispatched[#dispatched+1] = r
651 local function _call(self, ...)
652 if #self.argv > 0 then
653 return getfenv()[self.name](unpack(self.argv), ...)
655 return getfenv()[self.name](...)
659 --- Create a function-call dispatching target.
660 -- @param name Target function of local controller
661 -- @param ... Additional parameters passed to the function
662 function call(name, ...)
663 return {type = "call", argv = {...}, name = name, target = _call}
667 local _template = function(self, ...)
668 require "luci.template".render(self.view)
671 --- Create a template render dispatching target.
672 -- @param name Template to be rendered
673 function template(name)
674 return {type = "template", view = name, target = _template}
678 local function _cbi(self, ...)
679 local cbi = require "luci.cbi"
680 local tpl = require "luci.template"
681 local http = require "luci.http"
683 local config = self.config or {}
684 local maps = cbi.load(self.model, ...)
688 for i, res in ipairs(maps) do
690 local cstate = res:parse()
691 if cstate and (not state or cstate < state) then
696 local function _resolve_path(path)
697 return type(path) == "table" and build_url(unpack(path)) or path
700 if config.on_valid_to and state and state > 0 and state < 2 then
701 http.redirect(_resolve_path(config.on_valid_to))
705 if config.on_changed_to and state and state > 1 then
706 http.redirect(_resolve_path(config.on_changed_to))
710 if config.on_success_to and state and state > 0 then
711 http.redirect(_resolve_path(config.on_success_to))
715 if config.state_handler then
716 if not config.state_handler(state, maps) then
721 http.header("X-CBI-State", state or 0)
723 if not config.noheader then
724 tpl.render("cbi/header", {state = state})
729 local applymap = false
730 local pageaction = true
731 local parsechain = { }
733 for i, res in ipairs(maps) do
734 if res.apply_needed and res.parsechain then
736 for _, c in ipairs(res.parsechain) do
737 parsechain[#parsechain+1] = c
743 redirect = redirect or res.redirect
746 if res.pageaction == false then
751 messages = messages or { }
752 messages[#messages+1] = res.message
756 for i, res in ipairs(maps) do
762 pageaction = pageaction,
763 parsechain = parsechain
767 if not config.nofooter then
768 tpl.render("cbi/footer", {
770 pageaction = pageaction,
773 autoapply = config.autoapply
778 --- Create a CBI model dispatching target.
779 -- @param model CBI model to be rendered
780 function cbi(model, config)
781 return {type = "cbi", config = config, model = model, target = _cbi}
785 local function _arcombine(self, ...)
787 local target = #argv > 0 and self.targets[2] or self.targets[1]
788 setfenv(target.target, self.env)
789 target:target(unpack(argv))
792 --- Create a combined dispatching target for non argv and argv requests.
793 -- @param trg1 Overview Target
794 -- @param trg2 Detail Target
795 function arcombine(trg1, trg2)
796 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
800 local function _form(self, ...)
801 local cbi = require "luci.cbi"
802 local tpl = require "luci.template"
803 local http = require "luci.http"
805 local maps = luci.cbi.load(self.model, ...)
808 for i, res in ipairs(maps) do
809 local cstate = res:parse()
810 if cstate and (not state or cstate < state) then
815 http.header("X-CBI-State", state or 0)
817 for i, res in ipairs(maps) do
823 --- Create a CBI form model dispatching target.
824 -- @param model CBI form model tpo be rendered
826 return {type = "cbi", model = model, target = _form}