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 "luci.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()
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 local viewns = setmetatable({}, {__index=function(table, key)
227 if key == "controller" then
229 elseif key == "REQUEST_URI" then
230 return build_url(unpack(ctx.requestpath))
232 return rawget(table, key) or _G[key]
235 tpl.context.viewns = viewns
236 viewns.write = luci.http.write
237 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
238 viewns.translate = function(...) return require("luci.i18n").translate(...) end
239 viewns.striptags = util.striptags
241 viewns.theme = fs.basename(media)
242 viewns.resource = luci.config.main.resourcebase
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
277 if not util.contains(accs, user) then
279 ctx.urltoken.stok = nil
280 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
281 if not user or not util.contains(accs, user) then
284 local sid = sess or luci.sys.uniqueid(16)
286 local token = luci.sys.uniqueid(16)
287 sauth.write(sid, util.get_bytecode({
290 secret=luci.sys.uniqueid(16)
292 ctx.urltoken.stok = token
294 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
295 ctx.authsession = sid
298 luci.http.status(403, "Forbidden")
302 ctx.authsession = sess
306 if track.setgroup then
307 luci.sys.process.setgroup(track.setgroup)
310 if track.setuser then
311 luci.sys.process.setuser(track.setuser)
316 if type(c.target) == "function" then
318 elseif type(c.target) == "table" then
319 target = c.target.target
323 if c and (c.index or type(target) == "function") then
325 ctx.requested = ctx.requested or ctx.dispatched
328 if c and c.index then
329 local tpl = require "luci.template"
331 if util.copcall(tpl.render, "indexer", {}) then
336 if type(target) == "function" then
337 util.copcall(function()
338 local oldenv = getfenv(target)
339 local module = require(c.module)
340 local env = setmetatable({}, {__index=
343 return rawget(tbl, key) or module[key] or oldenv[key]
349 if type(c.target) == "table" then
350 target(c.target, unpack(args))
359 --- Generate the dispatching index using the best possible strategy.
360 function createindex()
361 local path = luci.util.libpath() .. "/controller/"
362 local suff = { ".lua", ".lua.gz" }
364 if luci.util.copcall(require, "luci.fastindex") then
365 createindex_fastindex(path, suff)
367 createindex_plain(path, suff)
371 --- Generate the dispatching index using the fastindex C-indexer.
372 -- @param path Controller base directory
373 -- @param suffixes Controller file suffixes
374 function createindex_fastindex(path, suffixes)
378 fi = luci.fastindex.new("index")
379 for _, suffix in ipairs(suffixes) do
380 fi.add(path .. "*" .. suffix)
381 fi.add(path .. "*/*" .. suffix)
386 for k, v in pairs(fi.indexes) do
391 --- Generate the dispatching index using the native file-cache based strategy.
392 -- @param path Controller base directory
393 -- @param suffixes Controller file suffixes
394 function createindex_plain(path, suffixes)
395 local controllers = { }
396 for _, suffix in ipairs(suffixes) do
397 controllers = util.combine(
399 luci.fs.glob(path .. "*" .. suffix) or {},
400 luci.fs.glob(path .. "*/*" .. suffix) or {}
405 local cachedate = fs.mtime(indexcache)
408 for _, obj in ipairs(controllers) do
409 local omtime = fs.mtime(path .. "/" .. obj)
410 realdate = (omtime and omtime > realdate) and omtime or realdate
413 if cachedate > realdate then
415 sys.process.info("uid") == fs.stat(indexcache, "uid")
416 and fs.stat(indexcache, "modestr") == "rw-------",
417 "Fatal: Indexcache is not sane!"
420 index = loadfile(indexcache)()
428 for i,c in ipairs(controllers) do
429 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
430 for _, suffix in ipairs(suffixes) do
431 module = module:gsub(suffix.."$", "")
434 local mod = require(module)
435 local idx = mod.index
437 if type(idx) == "function" then
443 local f = nixio.open(indexcache, "w", 600)
444 f:writeall(util.get_bytecode(index))
449 --- Create the dispatching tree from the index.
450 -- Build the index before if it does not exist yet.
451 function createtree()
457 local tree = {nodes={}}
460 ctx.treecache = setmetatable({}, {__mode="v"})
464 -- Load default translation
465 require "luci.i18n".loadc("default")
467 local scope = setmetatable({}, {__index = luci.dispatcher})
469 for k, v in pairs(index) do
475 local function modisort(a,b)
476 return modi[a].order < modi[b].order
479 for _, v in util.spairs(modi, modisort) do
480 scope._NAME = v.module
481 setfenv(v.func, scope)
488 --- Register a tree modifier.
489 -- @param func Modifier function
490 -- @param order Modifier order value (optional)
491 function modifier(func, order)
492 context.modifiers[#context.modifiers+1] = {
500 --- Clone a node of the dispatching tree to another position.
501 -- @param path Virtual path destination
502 -- @param clone Virtual path source
503 -- @param title Destination node title (optional)
504 -- @param order Destination node order value (optional)
505 -- @return Dispatching tree node
506 function assign(path, clone, title, order)
507 local obj = node(unpack(path))
514 setmetatable(obj, {__index = _create_node(clone)})
519 --- Create a new dispatching node and define common parameters.
520 -- @param path Virtual path
521 -- @param target Target function to call when dispatched.
522 -- @param title Destination node title
523 -- @param order Destination node order value (optional)
524 -- @return Dispatching tree node
525 function entry(path, target, title, order)
526 local c = node(unpack(path))
531 c.module = getfenv(2)._NAME
536 --- Fetch or create a dispatching node without setting the target module or
537 -- enabling the node.
538 -- @param ... Virtual path
539 -- @return Dispatching tree node
541 return _create_node({...})
544 --- Fetch or create a new dispatching node.
545 -- @param ... Virtual path
546 -- @return Dispatching tree node
548 local c = _create_node({...})
550 c.module = getfenv(2)._NAME
556 function _create_node(path, cache)
561 cache = cache or context.treecache
562 local name = table.concat(path, ".")
563 local c = cache[name]
566 local new = {nodes={}, auto=true, path=util.clone(path)}
567 local last = table.remove(path)
569 c = _create_node(path, cache)
582 --- Create a redirect to another dispatching node.
583 -- @param ... Virtual path destination
587 for _, r in ipairs({...}) do
595 --- Rewrite the first x path values of the request.
596 -- @param n Number of path values to replace
597 -- @param ... Virtual path to replace removed path values with
598 function rewrite(n, ...)
601 local dispatched = util.clone(context.dispatched)
604 table.remove(dispatched, 1)
607 for i, r in ipairs(req) do
608 table.insert(dispatched, i, r)
611 for _, r in ipairs({...}) do
612 dispatched[#dispatched+1] = r
620 local function _call(self, ...)
621 if #self.argv > 0 then
622 return getfenv()[self.name](unpack(self.argv), ...)
624 return getfenv()[self.name](...)
628 --- Create a function-call dispatching target.
629 -- @param name Target function of local controller
630 -- @param ... Additional parameters passed to the function
631 function call(name, ...)
632 return {type = "call", argv = {...}, name = name, target = _call}
636 local _template = function(self, ...)
637 require "luci.template".render(self.view)
640 --- Create a template render dispatching target.
641 -- @param name Template to be rendered
642 function template(name)
643 return {type = "template", view = name, target = _template}
647 local function _cbi(self, ...)
648 local cbi = require "luci.cbi"
649 local tpl = require "luci.template"
650 local http = require "luci.http"
652 local config = self.config or {}
653 local maps = cbi.load(self.model, ...)
657 for i, res in ipairs(maps) do
659 local cstate = res:parse()
660 if cstate and (not state or cstate < state) then
665 local function _resolve_path(path)
666 return type(path) == "table" and build_url(unpack(path)) or path
669 if config.on_valid_to and state and state > 0 and state < 2 then
670 http.redirect(_resolve_path(config.on_valid_to))
674 if config.on_changed_to and state and state > 1 then
675 http.redirect(_resolve_path(config.on_changed_to))
679 if config.on_success_to and state and state > 0 then
680 http.redirect(_resolve_path(config.on_success_to))
684 if config.state_handler then
685 if not config.state_handler(state, maps) then
690 local pageaction = true
691 http.header("X-CBI-State", state or 0)
692 if not config.noheader then
693 tpl.render("cbi/header", {state = state})
695 for i, res in ipairs(maps) do
697 if res.pageaction == false then
701 if not config.nofooter then
702 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
706 --- Create a CBI model dispatching target.
707 -- @param model CBI model to be rendered
708 function cbi(model, config)
709 return {type = "cbi", config = config, model = model, target = _cbi}
713 local function _arcombine(self, ...)
715 local target = #argv > 0 and self.targets[2] or self.targets[1]
716 setfenv(target.target, self.env)
717 target:target(unpack(argv))
720 --- Create a combined dispatching target for non argv and argv requests.
721 -- @param trg1 Overview Target
722 -- @param trg2 Detail Target
723 function arcombine(trg1, trg2)
724 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
728 local function _form(self, ...)
729 local cbi = require "luci.cbi"
730 local tpl = require "luci.template"
731 local http = require "luci.http"
733 local maps = luci.cbi.load(self.model, ...)
736 for i, res in ipairs(maps) do
737 local cstate = res:parse()
738 if cstate and (not state or cstate < state) then
743 http.header("X-CBI-State", state or 0)
745 for i, res in ipairs(maps) do
751 --- Create a CBI form model dispatching target.
752 -- @param model CBI form model tpo be rendered
754 return {type = "cbi", model = model, target = _form}