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)
112 luci.http.context.request = request
116 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
119 for _, node in ipairs(prefix) do
124 for node in pathinfo:gmatch("[^/]+") do
128 local stat, err = util.coxpcall(function()
129 dispatch(context.request)
134 --context._disable_memtrace()
137 --- Dispatches a LuCI virtual path.
138 -- @param request Virtual path
139 function dispatch(request)
140 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
143 ctx.urltoken = ctx.urltoken or {}
145 local conf = require "luci.config"
147 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
149 local lang = conf.main.lang
150 if lang == "auto" then
151 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
152 for lpat in aclang:gmatch("[%w-]+") do
153 lpat = lpat and lpat:gsub("-", "_")
154 if conf.languages[lpat] then
160 require "luci.i18n".setlanguage(lang)
171 ctx.requestargs = ctx.requestargs or args
174 local token = ctx.urltoken
178 for i, s in ipairs(request) do
181 tkey, tval = s:match(";(%w+)=(.*)")
196 util.update(track, c)
205 for j=n+1, #request do
206 args[#args+1] = request[j]
207 freq[#freq+1] = request[j]
211 ctx.requestpath = freq
215 require("luci.i18n").loadc(track.i18n)
218 -- Init template engine
219 if (c and c.index) or not track.notemplate then
220 local tpl = require("luci.template")
221 local media = track.mediaurlbase or luci.config.main.mediaurlbase
222 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
223 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
225 for name, theme in pairs(luci.config.themes) do
226 if name:sub(1,1) ~= "." and pcall(tpl.Template,
227 "themes/%s/header" % fs.basename(theme)) then
231 assert(media, "No valid theme found")
234 tpl.context.viewns = setmetatable({
235 write = luci.http.write;
236 include = function(name) tpl.Template(name):render(getfenv(2)) end;
237 translate = function(...) return require("luci.i18n").translate(...) end;
238 striptags = util.striptags;
240 theme = fs.basename(media);
241 resource = luci.config.main.resourcebase
242 }, {__index=function(table, key)
243 if key == "controller" then
245 elseif key == "REQUEST_URI" then
246 return build_url(unpack(ctx.requestpath))
248 return rawget(table, key) or _G[key]
253 track.dependent = (track.dependent ~= false)
254 assert(not track.dependent or not track.auto, "Access Violation")
256 if track.sysauth then
257 local sauth = require "luci.sauth"
259 local authen = type(track.sysauth_authenticator) == "function"
260 and track.sysauth_authenticator
261 or authenticator[track.sysauth_authenticator]
263 local def = (type(track.sysauth) == "string") and track.sysauth
264 local accs = def and {track.sysauth} or track.sysauth
265 local sess = ctx.authsession
266 local verifytoken = false
268 sess = luci.http.getcookie("sysauth")
269 sess = sess and sess:match("^[a-f0-9]*$")
273 local sdat = sauth.read(sess)
277 sdat = loadstring(sdat)
280 if not verifytoken or ctx.urltoken.stok == sdat.token then
284 local eu = http.getenv("HTTP_AUTH_USER")
285 local ep = http.getenv("HTTP_AUTH_PASS")
286 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
287 authen = function() return eu end
291 if not util.contains(accs, user) then
293 ctx.urltoken.stok = nil
294 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
295 if not user or not util.contains(accs, user) then
298 local sid = sess or luci.sys.uniqueid(16)
300 local token = luci.sys.uniqueid(16)
301 sauth.write(sid, util.get_bytecode({
304 secret=luci.sys.uniqueid(16)
306 ctx.urltoken.stok = token
308 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
309 ctx.authsession = sid
312 luci.http.status(403, "Forbidden")
316 ctx.authsession = sess
320 if track.setgroup then
321 luci.sys.process.setgroup(track.setgroup)
324 if track.setuser then
325 luci.sys.process.setuser(track.setuser)
330 if type(c.target) == "function" then
332 elseif type(c.target) == "table" then
333 target = c.target.target
337 if c and (c.index or type(target) == "function") then
339 ctx.requested = ctx.requested or ctx.dispatched
342 if c and c.index then
343 local tpl = require "luci.template"
345 if util.copcall(tpl.render, "indexer", {}) then
350 if type(target) == "function" then
351 util.copcall(function()
352 local oldenv = getfenv(target)
353 local module = require(c.module)
354 local env = setmetatable({}, {__index=
357 return rawget(tbl, key) or module[key] or oldenv[key]
363 if type(c.target) == "table" then
364 target(c.target, unpack(args))
373 --- Generate the dispatching index using the best possible strategy.
374 function createindex()
375 local path = luci.util.libpath() .. "/controller/"
376 local suff = { ".lua", ".lua.gz" }
378 if luci.util.copcall(require, "luci.fastindex") then
379 createindex_fastindex(path, suff)
381 createindex_plain(path, suff)
385 --- Generate the dispatching index using the fastindex C-indexer.
386 -- @param path Controller base directory
387 -- @param suffixes Controller file suffixes
388 function createindex_fastindex(path, suffixes)
392 fi = luci.fastindex.new("index")
393 for _, suffix in ipairs(suffixes) do
394 fi.add(path .. "*" .. suffix)
395 fi.add(path .. "*/*" .. suffix)
400 for k, v in pairs(fi.indexes) do
405 --- Generate the dispatching index using the native file-cache based strategy.
406 -- @param path Controller base directory
407 -- @param suffixes Controller file suffixes
408 function createindex_plain(path, suffixes)
409 local controllers = { }
410 for _, suffix in ipairs(suffixes) do
411 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
412 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
416 local cachedate = fs.stat(indexcache, "mtime")
419 for _, obj in ipairs(controllers) do
420 local omtime = fs.stat(path .. "/" .. obj, "mtime")
421 realdate = (omtime and omtime > realdate) and omtime or realdate
424 if cachedate > realdate then
426 sys.process.info("uid") == fs.stat(indexcache, "uid")
427 and fs.stat(indexcache, "modestr") == "rw-------",
428 "Fatal: Indexcache is not sane!"
431 index = loadfile(indexcache)()
439 for i,c in ipairs(controllers) do
440 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
441 for _, suffix in ipairs(suffixes) do
442 module = module:gsub(suffix.."$", "")
445 local mod = require(module)
446 local idx = mod.index
448 if type(idx) == "function" then
454 local f = nixio.open(indexcache, "w", 600)
455 f:writeall(util.get_bytecode(index))
460 --- Create the dispatching tree from the index.
461 -- Build the index before if it does not exist yet.
462 function createtree()
468 local tree = {nodes={}}
471 ctx.treecache = setmetatable({}, {__mode="v"})
475 -- Load default translation
476 require "luci.i18n".loadc("default")
478 local scope = setmetatable({}, {__index = luci.dispatcher})
480 for k, v in pairs(index) do
486 local function modisort(a,b)
487 return modi[a].order < modi[b].order
490 for _, v in util.spairs(modi, modisort) do
491 scope._NAME = v.module
492 setfenv(v.func, scope)
499 --- Register a tree modifier.
500 -- @param func Modifier function
501 -- @param order Modifier order value (optional)
502 function modifier(func, order)
503 context.modifiers[#context.modifiers+1] = {
511 --- Clone a node of the dispatching tree to another position.
512 -- @param path Virtual path destination
513 -- @param clone Virtual path source
514 -- @param title Destination node title (optional)
515 -- @param order Destination node order value (optional)
516 -- @return Dispatching tree node
517 function assign(path, clone, title, order)
518 local obj = node(unpack(path))
525 setmetatable(obj, {__index = _create_node(clone)})
530 --- Create a new dispatching node and define common parameters.
531 -- @param path Virtual path
532 -- @param target Target function to call when dispatched.
533 -- @param title Destination node title
534 -- @param order Destination node order value (optional)
535 -- @return Dispatching tree node
536 function entry(path, target, title, order)
537 local c = node(unpack(path))
542 c.module = getfenv(2)._NAME
547 --- Fetch or create a dispatching node without setting the target module or
548 -- enabling the node.
549 -- @param ... Virtual path
550 -- @return Dispatching tree node
552 return _create_node({...})
555 --- Fetch or create a new dispatching node.
556 -- @param ... Virtual path
557 -- @return Dispatching tree node
559 local c = _create_node({...})
561 c.module = getfenv(2)._NAME
567 function _create_node(path, cache)
572 cache = cache or context.treecache
573 local name = table.concat(path, ".")
574 local c = cache[name]
577 local new = {nodes={}, auto=true, path=util.clone(path)}
578 local last = table.remove(path)
580 c = _create_node(path, cache)
593 --- Create a redirect to another dispatching node.
594 -- @param ... Virtual path destination
598 for _, r in ipairs({...}) do
606 --- Rewrite the first x path values of the request.
607 -- @param n Number of path values to replace
608 -- @param ... Virtual path to replace removed path values with
609 function rewrite(n, ...)
612 local dispatched = util.clone(context.dispatched)
615 table.remove(dispatched, 1)
618 for i, r in ipairs(req) do
619 table.insert(dispatched, i, r)
622 for _, r in ipairs({...}) do
623 dispatched[#dispatched+1] = r
631 local function _call(self, ...)
632 if #self.argv > 0 then
633 return getfenv()[self.name](unpack(self.argv), ...)
635 return getfenv()[self.name](...)
639 --- Create a function-call dispatching target.
640 -- @param name Target function of local controller
641 -- @param ... Additional parameters passed to the function
642 function call(name, ...)
643 return {type = "call", argv = {...}, name = name, target = _call}
647 local _template = function(self, ...)
648 require "luci.template".render(self.view)
651 --- Create a template render dispatching target.
652 -- @param name Template to be rendered
653 function template(name)
654 return {type = "template", view = name, target = _template}
658 local function _cbi(self, ...)
659 local cbi = require "luci.cbi"
660 local tpl = require "luci.template"
661 local http = require "luci.http"
663 local config = self.config or {}
664 local maps = cbi.load(self.model, ...)
668 for i, res in ipairs(maps) do
670 local cstate = res:parse()
671 if cstate and (not state or cstate < state) then
676 local function _resolve_path(path)
677 return type(path) == "table" and build_url(unpack(path)) or path
680 if config.on_valid_to and state and state > 0 and state < 2 then
681 http.redirect(_resolve_path(config.on_valid_to))
685 if config.on_changed_to and state and state > 1 then
686 http.redirect(_resolve_path(config.on_changed_to))
690 if config.on_success_to and state and state > 0 then
691 http.redirect(_resolve_path(config.on_success_to))
695 if config.state_handler then
696 if not config.state_handler(state, maps) then
701 local pageaction = true
702 http.header("X-CBI-State", state or 0)
703 if not config.noheader then
704 tpl.render("cbi/header", {state = state})
706 for i, res in ipairs(maps) do
708 if res.pageaction == false then
712 if not config.nofooter then
713 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
717 --- Create a CBI model dispatching target.
718 -- @param model CBI model to be rendered
719 function cbi(model, config)
720 return {type = "cbi", config = config, model = model, target = _cbi}
724 local function _arcombine(self, ...)
726 local target = #argv > 0 and self.targets[2] or self.targets[1]
727 setfenv(target.target, self.env)
728 target:target(unpack(argv))
731 --- Create a combined dispatching target for non argv and argv requests.
732 -- @param trg1 Overview Target
733 -- @param trg2 Detail Target
734 function arcombine(trg1, trg2)
735 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
739 local function _form(self, ...)
740 local cbi = require "luci.cbi"
741 local tpl = require "luci.template"
742 local http = require "luci.http"
744 local maps = luci.cbi.load(self.model, ...)
747 for i, res in ipairs(maps) do
748 local cstate = res:parse()
749 if cstate and (not state or cstate < state) then
754 http.header("X-CBI-State", state or 0)
756 for i, res in ipairs(maps) do
762 --- Create a CBI form model dispatching target.
763 -- @param model CBI form model tpo be rendered
765 return {type = "cbi", model = model, target = _form}