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 sn = http.getenv("SCRIPT_NAME") or ""
56 for k, v in pairs(context.urltoken) do
57 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
59 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
62 --- Send a 404 error code and render the "error404" template if available.
63 -- @param message Custom error message (optional)
65 function error404(message)
66 luci.http.status(404, "Not Found")
67 message = message or "Not Found"
69 require("luci.template")
70 if not luci.util.copcall(luci.template.render, "error404") then
71 luci.http.prepare_content("text/plain")
72 luci.http.write(message)
77 --- Send a 500 error code and render the "error500" template if available.
78 -- @param message Custom error message (optional)#
80 function error500(message)
81 luci.util.perror(message)
82 if not context.template_header_sent then
83 luci.http.status(500, "Internal Server Error")
84 luci.http.prepare_content("text/plain")
85 luci.http.write(message)
87 require("luci.template")
88 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
89 luci.http.prepare_content("text/plain")
90 luci.http.write(message)
96 function authenticator.htmlauth(validator, accs, default)
97 local user = luci.http.formvalue("username")
98 local pass = luci.http.formvalue("password")
100 if user and validator(user, pass) then
105 require("luci.template")
107 luci.template.render("sysauth", {duser=default, fuser=user})
112 --- Dispatch an HTTP request.
113 -- @param request LuCI HTTP Request object
114 function httpdispatch(request, prefix)
115 luci.http.context.request = request
119 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
122 for _, node in ipairs(prefix) do
127 for node in pathinfo:gmatch("[^/]+") do
131 local stat, err = util.coxpcall(function()
132 dispatch(context.request)
137 --context._disable_memtrace()
140 --- Dispatches a LuCI virtual path.
141 -- @param request Virtual path
142 function dispatch(request)
143 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
146 ctx.urltoken = ctx.urltoken or {}
148 local conf = require "luci.config"
150 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
152 local lang = conf.main.lang
153 if lang == "auto" then
154 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
155 for lpat in aclang:gmatch("[%w-]+") do
156 lpat = lpat and lpat:gsub("-", "_")
157 if conf.languages[lpat] then
163 require "luci.i18n".setlanguage(lang)
174 ctx.requestargs = ctx.requestargs or args
177 local token = ctx.urltoken
181 for i, s in ipairs(request) do
184 tkey, tval = s:match(";(%w+)=(.*)")
199 util.update(track, c)
208 for j=n+1, #request do
209 args[#args+1] = request[j]
210 freq[#freq+1] = request[j]
214 ctx.requestpath = ctx.requestpath or freq
218 require("luci.i18n").loadc(track.i18n)
221 -- Init template engine
222 if (c and c.index) or not track.notemplate then
223 local tpl = require("luci.template")
224 local media = track.mediaurlbase or luci.config.main.mediaurlbase
225 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
227 for name, theme in pairs(luci.config.themes) do
228 if name:sub(1,1) ~= "." and pcall(tpl.Template,
229 "themes/%s/header" % fs.basename(theme)) then
233 assert(media, "No valid theme found")
236 tpl.context.viewns = setmetatable({
237 write = luci.http.write;
238 include = function(name) tpl.Template(name):render(getfenv(2)) end;
239 translate = function(...) return require("luci.i18n").translate(...) end;
240 striptags = util.striptags;
241 pcdata = util.pcdata;
243 theme = fs.basename(media);
244 resource = luci.config.main.resourcebase
245 }, {__index=function(table, key)
246 if key == "controller" then
248 elseif key == "REQUEST_URI" then
249 return build_url(unpack(ctx.requestpath))
251 return rawget(table, key) or _G[key]
256 track.dependent = (track.dependent ~= false)
257 assert(not track.dependent or not track.auto, "Access Violation")
259 if track.sysauth then
260 local sauth = require "luci.sauth"
262 local authen = type(track.sysauth_authenticator) == "function"
263 and track.sysauth_authenticator
264 or authenticator[track.sysauth_authenticator]
266 local def = (type(track.sysauth) == "string") and track.sysauth
267 local accs = def and {track.sysauth} or track.sysauth
268 local sess = ctx.authsession
269 local verifytoken = false
271 sess = luci.http.getcookie("sysauth")
272 sess = sess and sess:match("^[a-f0-9]*$")
276 local sdat = sauth.read(sess)
280 sdat = loadstring(sdat)
283 if not verifytoken or ctx.urltoken.stok == sdat.token then
287 local eu = http.getenv("HTTP_AUTH_USER")
288 local ep = http.getenv("HTTP_AUTH_PASS")
289 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
290 authen = function() return eu end
294 if not util.contains(accs, user) then
296 ctx.urltoken.stok = nil
297 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
298 if not user or not util.contains(accs, user) then
301 local sid = sess or luci.sys.uniqueid(16)
303 local token = luci.sys.uniqueid(16)
304 sauth.write(sid, util.get_bytecode({
307 secret=luci.sys.uniqueid(16)
309 ctx.urltoken.stok = token
311 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
312 ctx.authsession = sid
316 luci.http.status(403, "Forbidden")
320 ctx.authsession = sess
325 if track.setgroup then
326 luci.sys.process.setgroup(track.setgroup)
329 if track.setuser then
330 luci.sys.process.setuser(track.setuser)
335 if type(c.target) == "function" then
337 elseif type(c.target) == "table" then
338 target = c.target.target
342 if c and (c.index or type(target) == "function") then
344 ctx.requested = ctx.requested or ctx.dispatched
347 if c and c.index then
348 local tpl = require "luci.template"
350 if util.copcall(tpl.render, "indexer", {}) then
355 if type(target) == "function" then
356 util.copcall(function()
357 local oldenv = getfenv(target)
358 local module = require(c.module)
359 local env = setmetatable({}, {__index=
362 return rawget(tbl, key) or module[key] or oldenv[key]
368 if type(c.target) == "table" then
369 target(c.target, unpack(args))
378 --- Generate the dispatching index using the best possible strategy.
379 function createindex()
380 local path = luci.util.libpath() .. "/controller/"
381 local suff = { ".lua", ".lua.gz" }
383 if luci.util.copcall(require, "luci.fastindex") then
384 createindex_fastindex(path, suff)
386 createindex_plain(path, suff)
390 --- Generate the dispatching index using the fastindex C-indexer.
391 -- @param path Controller base directory
392 -- @param suffixes Controller file suffixes
393 function createindex_fastindex(path, suffixes)
397 fi = luci.fastindex.new("index")
398 for _, suffix in ipairs(suffixes) do
399 fi.add(path .. "*" .. suffix)
400 fi.add(path .. "*/*" .. suffix)
405 for k, v in pairs(fi.indexes) do
410 --- Generate the dispatching index using the native file-cache based strategy.
411 -- @param path Controller base directory
412 -- @param suffixes Controller file suffixes
413 function createindex_plain(path, suffixes)
414 local controllers = { }
415 for _, suffix in ipairs(suffixes) do
416 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
417 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
421 local cachedate = fs.stat(indexcache, "mtime")
424 for _, obj in ipairs(controllers) do
425 local omtime = fs.stat(path .. "/" .. obj, "mtime")
426 realdate = (omtime and omtime > realdate) and omtime or realdate
429 if cachedate > realdate then
431 sys.process.info("uid") == fs.stat(indexcache, "uid")
432 and fs.stat(indexcache, "modestr") == "rw-------",
433 "Fatal: Indexcache is not sane!"
436 index = loadfile(indexcache)()
444 for i,c in ipairs(controllers) do
445 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
446 for _, suffix in ipairs(suffixes) do
447 module = module:gsub(suffix.."$", "")
450 local mod = require(module)
451 local idx = mod.index
453 if type(idx) == "function" then
459 local f = nixio.open(indexcache, "w", 600)
460 f:writeall(util.get_bytecode(index))
465 --- Create the dispatching tree from the index.
466 -- Build the index before if it does not exist yet.
467 function createtree()
473 local tree = {nodes={}}
476 ctx.treecache = setmetatable({}, {__mode="v"})
480 -- Load default translation
481 require "luci.i18n".loadc("base")
483 local scope = setmetatable({}, {__index = luci.dispatcher})
485 for k, v in pairs(index) do
491 local function modisort(a,b)
492 return modi[a].order < modi[b].order
495 for _, v in util.spairs(modi, modisort) do
496 scope._NAME = v.module
497 setfenv(v.func, scope)
504 --- Register a tree modifier.
505 -- @param func Modifier function
506 -- @param order Modifier order value (optional)
507 function modifier(func, order)
508 context.modifiers[#context.modifiers+1] = {
516 --- Clone a node of the dispatching tree to another position.
517 -- @param path Virtual path destination
518 -- @param clone Virtual path source
519 -- @param title Destination node title (optional)
520 -- @param order Destination node order value (optional)
521 -- @return Dispatching tree node
522 function assign(path, clone, title, order)
523 local obj = node(unpack(path))
530 setmetatable(obj, {__index = _create_node(clone)})
535 --- Create a new dispatching node and define common parameters.
536 -- @param path Virtual path
537 -- @param target Target function to call when dispatched.
538 -- @param title Destination node title
539 -- @param order Destination node order value (optional)
540 -- @return Dispatching tree node
541 function entry(path, target, title, order)
542 local c = node(unpack(path))
547 c.module = getfenv(2)._NAME
552 --- Fetch or create a dispatching node without setting the target module or
553 -- enabling the node.
554 -- @param ... Virtual path
555 -- @return Dispatching tree node
557 return _create_node({...})
560 --- Fetch or create a new dispatching node.
561 -- @param ... Virtual path
562 -- @return Dispatching tree node
564 local c = _create_node({...})
566 c.module = getfenv(2)._NAME
572 function _create_node(path, cache)
577 cache = cache or context.treecache
578 local name = table.concat(path, ".")
579 local c = cache[name]
582 local new = {nodes={}, auto=true, path=util.clone(path)}
583 local last = table.remove(path)
585 c = _create_node(path, cache)
598 --- Create a redirect to another dispatching node.
599 -- @param ... Virtual path destination
603 for _, r in ipairs({...}) do
611 --- Rewrite the first x path values of the request.
612 -- @param n Number of path values to replace
613 -- @param ... Virtual path to replace removed path values with
614 function rewrite(n, ...)
617 local dispatched = util.clone(context.dispatched)
620 table.remove(dispatched, 1)
623 for i, r in ipairs(req) do
624 table.insert(dispatched, i, r)
627 for _, r in ipairs({...}) do
628 dispatched[#dispatched+1] = r
636 local function _call(self, ...)
637 if #self.argv > 0 then
638 return getfenv()[self.name](unpack(self.argv), ...)
640 return getfenv()[self.name](...)
644 --- Create a function-call dispatching target.
645 -- @param name Target function of local controller
646 -- @param ... Additional parameters passed to the function
647 function call(name, ...)
648 return {type = "call", argv = {...}, name = name, target = _call}
652 local _template = function(self, ...)
653 require "luci.template".render(self.view)
656 --- Create a template render dispatching target.
657 -- @param name Template to be rendered
658 function template(name)
659 return {type = "template", view = name, target = _template}
663 local function _cbi(self, ...)
664 local cbi = require "luci.cbi"
665 local tpl = require "luci.template"
666 local http = require "luci.http"
668 local config = self.config or {}
669 local maps = cbi.load(self.model, ...)
673 for i, res in ipairs(maps) do
675 local cstate = res:parse()
676 if cstate and (not state or cstate < state) then
681 local function _resolve_path(path)
682 return type(path) == "table" and build_url(unpack(path)) or path
685 if config.on_valid_to and state and state > 0 and state < 2 then
686 http.redirect(_resolve_path(config.on_valid_to))
690 if config.on_changed_to and state and state > 1 then
691 http.redirect(_resolve_path(config.on_changed_to))
695 if config.on_success_to and state and state > 0 then
696 http.redirect(_resolve_path(config.on_success_to))
700 if config.state_handler then
701 if not config.state_handler(state, maps) then
706 local pageaction = true
707 http.header("X-CBI-State", state or 0)
708 if not config.noheader then
709 tpl.render("cbi/header", {state = state})
711 for i, res in ipairs(maps) do
713 if res.pageaction == false then
717 if not config.nofooter then
718 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
722 --- Create a CBI model dispatching target.
723 -- @param model CBI model to be rendered
724 function cbi(model, config)
725 return {type = "cbi", config = config, model = model, target = _cbi}
729 local function _arcombine(self, ...)
731 local target = #argv > 0 and self.targets[2] or self.targets[1]
732 setfenv(target.target, self.env)
733 target:target(unpack(argv))
736 --- Create a combined dispatching target for non argv and argv requests.
737 -- @param trg1 Overview Target
738 -- @param trg2 Detail Target
739 function arcombine(trg1, trg2)
740 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
744 local function _form(self, ...)
745 local cbi = require "luci.cbi"
746 local tpl = require "luci.template"
747 local http = require "luci.http"
749 local maps = luci.cbi.load(self.model, ...)
752 for i, res in ipairs(maps) do
753 local cstate = res:parse()
754 if cstate and (not state or cstate < state) then
759 http.header("X-CBI-State", state or 0)
761 for i, res in ipairs(maps) do
767 --- Create a CBI form model dispatching target.
768 -- @param model CBI form model tpo be rendered
770 return {type = "cbi", model = model, target = _form}