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, prefix, ext_tree)
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, ext_tree)
126 --context._disable_memtrace()
129 --- Dispatches a LuCI virtual path.
130 -- @param request Virtual path
131 function dispatch(request, ext_tree)
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)
155 ctx.index, ctx.tree, ctx.treecache, ctx.modifiers = unpack(ext_tree)
156 elseif not ctx.tree then
165 ctx.requestargs = ctx.requestargs or args
168 local token = ctx.urltoken
172 for i, s in ipairs(request) do
175 tkey, tval = s:match(";(%w+)=(.*)")
190 util.update(track, c)
199 for j=n+1, #request do
200 args[#args+1] = request[j]
201 freq[#freq+1] = request[j]
205 ctx.requestpath = freq
209 require("luci.i18n").loadc(track.i18n)
212 -- Init template engine
213 if (c and c.index) or not track.notemplate then
214 local tpl = require("luci.template")
215 local media = track.mediaurlbase or luci.config.main.mediaurlbase
216 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
217 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
219 for name, theme in pairs(luci.config.themes) do
220 if name:sub(1,1) ~= "." and pcall(tpl.Template,
221 "themes/%s/header" % fs.basename(theme)) then
225 assert(media, "No valid theme found")
228 tpl.context.viewns = setmetatable({
229 write = luci.http.write;
230 include = function(name) tpl.Template(name):render(getfenv(2)) end;
231 translate = function(...) return require("luci.i18n").translate(...) end;
232 striptags = util.striptags;
234 theme = fs.basename(media);
235 resource = luci.config.main.resourcebase
236 }, {__index=function(table, key)
237 if key == "controller" then
239 elseif key == "REQUEST_URI" then
240 return build_url(unpack(ctx.requestpath))
242 return rawget(table, key) or _G[key]
247 track.dependent = (track.dependent ~= false)
248 assert(not track.dependent or not track.auto, "Access Violation")
250 if track.sysauth then
251 local sauth = require "luci.sauth"
253 local authen = type(track.sysauth_authenticator) == "function"
254 and track.sysauth_authenticator
255 or authenticator[track.sysauth_authenticator]
257 local def = (type(track.sysauth) == "string") and track.sysauth
258 local accs = def and {track.sysauth} or track.sysauth
259 local sess = ctx.authsession
260 local verifytoken = false
262 sess = luci.http.getcookie("sysauth")
263 sess = sess and sess:match("^[a-f0-9]+$")
267 local sdat = sauth.read(sess)
271 sdat = loadstring(sdat)
274 if not verifytoken or ctx.urltoken.stok == sdat.token then
279 if not util.contains(accs, user) then
281 ctx.urltoken.stok = nil
282 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
283 if not user or not util.contains(accs, user) then
286 local sid = sess or luci.sys.uniqueid(16)
288 local token = luci.sys.uniqueid(16)
289 sauth.write(sid, util.get_bytecode({
292 secret=luci.sys.uniqueid(16)
294 ctx.urltoken.stok = token
296 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
297 ctx.authsession = sid
300 luci.http.status(403, "Forbidden")
304 ctx.authsession = sess
308 if track.setgroup then
309 luci.sys.process.setgroup(track.setgroup)
312 if track.setuser then
313 luci.sys.process.setuser(track.setuser)
318 if type(c.target) == "function" then
320 elseif type(c.target) == "table" then
321 target = c.target.target
325 if c and (c.index or type(target) == "function") then
327 ctx.requested = ctx.requested or ctx.dispatched
330 if c and c.index then
331 local tpl = require "luci.template"
333 if util.copcall(tpl.render, "indexer", {}) then
338 if type(target) == "function" then
339 util.copcall(function()
340 local oldenv = getfenv(target)
341 local module = require(c.module)
342 local env = setmetatable({}, {__index=
345 return rawget(tbl, key) or module[key] or oldenv[key]
351 if type(c.target) == "table" then
352 target(c.target, unpack(args))
361 --- Generate the dispatching index using the best possible strategy.
362 function createindex()
363 local path = luci.util.libpath() .. "/controller/"
364 local suff = { ".lua", ".lua.gz" }
366 if luci.util.copcall(require, "luci.fastindex") then
367 return createindex_fastindex(path, suff)
369 return createindex_plain(path, suff)
373 --- Generate the dispatching index using the fastindex C-indexer.
374 -- @param path Controller base directory
375 -- @param suffixes Controller file suffixes
376 function createindex_fastindex(path, suffixes)
380 fi = luci.fastindex.new("index")
381 for _, suffix in ipairs(suffixes) do
382 fi.add(path .. "*" .. suffix)
383 fi.add(path .. "*/*" .. suffix)
388 for k, v in pairs(fi.indexes) do
395 --- Generate the dispatching index using the native file-cache based strategy.
396 -- @param path Controller base directory
397 -- @param suffixes Controller file suffixes
398 function createindex_plain(path, suffixes)
399 local controllers = { }
400 for _, suffix in ipairs(suffixes) do
401 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
402 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
406 local cachedate = fs.stat(indexcache, "mtime")
409 for _, obj in ipairs(controllers) do
410 local omtime = fs.stat(path .. "/" .. obj, "mtime")
411 realdate = (omtime and omtime > realdate) and omtime or realdate
414 if cachedate > realdate then
416 sys.process.info("uid") == fs.stat(indexcache, "uid")
417 and fs.stat(indexcache, "modestr") == "rw-------",
418 "Fatal: Indexcache is not sane!"
421 index = loadfile(indexcache)()
429 for i,c in ipairs(controllers) do
430 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
431 for _, suffix in ipairs(suffixes) do
432 module = module:gsub(suffix.."$", "")
435 local mod = require(module)
436 local idx = mod.index
438 if type(idx) == "function" then
444 local f = nixio.open(indexcache, "w", 600)
445 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()
456 local tree = {nodes={}}
457 local cache = setmetatable({}, {__mode="v"})
460 if not ctx.index then
461 ctx.index = createindex()
465 ctx.treecache = cache
468 -- Load default translation
469 require "luci.i18n".loadc("default")
471 local scope = setmetatable({}, {__index = luci.dispatcher})
473 for k, v in pairs(ctx.index) do
479 local function modisort(a,b)
480 return modi[a].order < modi[b].order
483 for _, v in util.spairs(modi, modisort) do
484 scope._NAME = v.module
485 setfenv(v.func, scope)
489 return { index, tree, cache, modi }
492 --- Register a tree modifier.
493 -- @param func Modifier function
494 -- @param order Modifier order value (optional)
495 function modifier(func, order)
496 context.modifiers[#context.modifiers+1] = {
504 --- Clone a node of the dispatching tree to another position.
505 -- @param path Virtual path destination
506 -- @param clone Virtual path source
507 -- @param title Destination node title (optional)
508 -- @param order Destination node order value (optional)
509 -- @return Dispatching tree node
510 function assign(path, clone, title, order)
511 local obj = node(unpack(path))
518 setmetatable(obj, {__index = _create_node(clone)})
523 --- Create a new dispatching node and define common parameters.
524 -- @param path Virtual path
525 -- @param target Target function to call when dispatched.
526 -- @param title Destination node title
527 -- @param order Destination node order value (optional)
528 -- @return Dispatching tree node
529 function entry(path, target, title, order)
530 local c = node(unpack(path))
535 c.module = getfenv(2)._NAME
540 --- Fetch or create a dispatching node without setting the target module or
541 -- enabling the node.
542 -- @param ... Virtual path
543 -- @return Dispatching tree node
545 return _create_node({...})
548 --- Fetch or create a new dispatching node.
549 -- @param ... Virtual path
550 -- @return Dispatching tree node
552 local c = _create_node({...})
554 c.module = getfenv(2)._NAME
560 function _create_node(path, cache)
565 cache = cache or context.treecache
566 local name = table.concat(path, ".")
567 local c = cache[name]
570 local new = {nodes={}, auto=true, path=util.clone(path)}
571 local last = table.remove(path)
573 c = _create_node(path, cache)
586 --- Create a redirect to another dispatching node.
587 -- @param ... Virtual path destination
591 for _, r in ipairs({...}) do
599 --- Rewrite the first x path values of the request.
600 -- @param n Number of path values to replace
601 -- @param ... Virtual path to replace removed path values with
602 function rewrite(n, ...)
605 local dispatched = util.clone(context.dispatched)
608 table.remove(dispatched, 1)
611 for i, r in ipairs(req) do
612 table.insert(dispatched, i, r)
615 for _, r in ipairs({...}) do
616 dispatched[#dispatched+1] = r
624 local function _call(self, ...)
625 if #self.argv > 0 then
626 return getfenv()[self.name](unpack(self.argv), ...)
628 return getfenv()[self.name](...)
632 --- Create a function-call dispatching target.
633 -- @param name Target function of local controller
634 -- @param ... Additional parameters passed to the function
635 function call(name, ...)
636 return {type = "call", argv = {...}, name = name, target = _call}
640 local _template = function(self, ...)
641 require "luci.template".render(self.view)
644 --- Create a template render dispatching target.
645 -- @param name Template to be rendered
646 function template(name)
647 return {type = "template", view = name, target = _template}
651 local function _cbi(self, ...)
652 local cbi = require "luci.cbi"
653 local tpl = require "luci.template"
654 local http = require "luci.http"
656 local config = self.config or {}
657 local maps = cbi.load(self.model, ...)
661 for i, res in ipairs(maps) do
663 local cstate = res:parse()
664 if cstate and (not state or cstate < state) then
669 local function _resolve_path(path)
670 return type(path) == "table" and build_url(unpack(path)) or path
673 if config.on_valid_to and state and state > 0 and state < 2 then
674 http.redirect(_resolve_path(config.on_valid_to))
678 if config.on_changed_to and state and state > 1 then
679 http.redirect(_resolve_path(config.on_changed_to))
683 if config.on_success_to and state and state > 0 then
684 http.redirect(_resolve_path(config.on_success_to))
688 if config.state_handler then
689 if not config.state_handler(state, maps) then
694 local pageaction = true
695 http.header("X-CBI-State", state or 0)
696 if not config.noheader then
697 tpl.render("cbi/header", {state = state})
699 for i, res in ipairs(maps) do
701 if res.pageaction == false then
705 if not config.nofooter then
706 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
710 --- Create a CBI model dispatching target.
711 -- @param model CBI model to be rendered
712 function cbi(model, config)
713 return {type = "cbi", config = config, model = model, target = _cbi}
717 local function _arcombine(self, ...)
719 local target = #argv > 0 and self.targets[2] or self.targets[1]
720 setfenv(target.target, self.env)
721 target:target(unpack(argv))
724 --- Create a combined dispatching target for non argv and argv requests.
725 -- @param trg1 Overview Target
726 -- @param trg2 Detail Target
727 function arcombine(trg1, trg2)
728 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
732 local function _form(self, ...)
733 local cbi = require "luci.cbi"
734 local tpl = require "luci.template"
735 local http = require "luci.http"
737 local maps = luci.cbi.load(self.model, ...)
740 for i, res in ipairs(maps) do
741 local cstate = res:parse()
742 if cstate and (not state or cstate < state) then
747 http.header("X-CBI-State", state or 0)
749 for i, res in ipairs(maps) do
755 --- Create a CBI form model dispatching target.
756 -- @param model CBI form model tpo be rendered
758 return {type = "cbi", model = model, target = _form}