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"
48 --- Build the URL relative to the server webroot from given virtual path.
49 -- @param ... Virtual path
50 -- @return Relative URL
51 function build_url(...)
53 local sn = http.getenv("SCRIPT_NAME") or ""
54 for k, v in pairs(context.urltoken) do
55 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
57 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
60 --- Send a 404 error code and render the "error404" template if available.
61 -- @param message Custom error message (optional)
63 function error404(message)
64 luci.http.status(404, "Not Found")
65 message = message or "Not Found"
67 require("luci.template")
68 if not luci.util.copcall(luci.template.render, "error404") then
69 luci.http.prepare_content("text/plain")
70 luci.http.write(message)
75 --- Send a 500 error code and render the "error500" template if available.
76 -- @param message Custom error message (optional)#
78 function error500(message)
79 luci.util.perror(message)
80 if not context.template_header_sent then
81 luci.http.status(500, "Internal Server Error")
82 luci.http.prepare_content("text/plain")
83 luci.http.write(message)
85 require("luci.template")
86 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
87 luci.http.prepare_content("text/plain")
88 luci.http.write(message)
94 function authenticator.htmlauth(validator, accs, default)
95 local user = luci.http.formvalue("username")
96 local pass = luci.http.formvalue("password")
98 if user and validator(user, pass) then
103 require("luci.template")
105 luci.template.render("sysauth", {duser=default, fuser=user})
110 --- Dispatch an HTTP request.
111 -- @param request LuCI HTTP Request object
112 function httpdispatch(request, prefix)
113 luci.http.context.request = request
117 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
120 for _, node in ipairs(prefix) do
125 for node in pathinfo:gmatch("[^/]+") do
129 local stat, err = util.coxpcall(function()
130 dispatch(context.request)
135 --context._disable_memtrace()
138 --- Dispatches a LuCI virtual path.
139 -- @param request Virtual path
140 function dispatch(request)
141 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
144 ctx.urltoken = ctx.urltoken or {}
146 local conf = require "luci.config"
148 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
150 local lang = conf.main.lang
151 if lang == "auto" then
152 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
153 for lpat in aclang:gmatch("[%w-]+") do
154 lpat = lpat and lpat:gsub("-", "_")
155 if conf.languages[lpat] then
161 require "luci.i18n".setlanguage(lang)
172 ctx.requestargs = ctx.requestargs or args
175 local token = ctx.urltoken
179 for i, s in ipairs(request) do
182 tkey, tval = s:match(";(%w+)=(.*)")
197 util.update(track, c)
206 for j=n+1, #request do
207 args[#args+1] = request[j]
208 freq[#freq+1] = request[j]
212 ctx.requestpath = ctx.requestpath or freq
216 require("luci.i18n").loadc(track.i18n)
219 -- Init template engine
220 if (c and c.index) or not track.notemplate then
221 local tpl = require("luci.template")
222 local media = track.mediaurlbase or luci.config.main.mediaurlbase
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;
239 pcdata = util.pcdata;
241 theme = fs.basename(media);
242 resource = luci.config.main.resourcebase
243 }, {__index=function(table, key)
244 if key == "controller" then
246 elseif key == "REQUEST_URI" then
247 return build_url(unpack(ctx.requestpath))
249 return rawget(table, key) or _G[key]
254 track.dependent = (track.dependent ~= false)
255 assert(not track.dependent or not track.auto, "Access Violation")
257 if track.sysauth then
258 local sauth = require "luci.sauth"
260 local authen = type(track.sysauth_authenticator) == "function"
261 and track.sysauth_authenticator
262 or authenticator[track.sysauth_authenticator]
264 local def = (type(track.sysauth) == "string") and track.sysauth
265 local accs = def and {track.sysauth} or track.sysauth
266 local sess = ctx.authsession
267 local verifytoken = false
269 sess = luci.http.getcookie("sysauth")
270 sess = sess and sess:match("^[a-f0-9]*$")
274 local sdat = sauth.read(sess)
278 sdat = loadstring(sdat)
281 if not verifytoken or ctx.urltoken.stok == sdat.token then
285 local eu = http.getenv("HTTP_AUTH_USER")
286 local ep = http.getenv("HTTP_AUTH_PASS")
287 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
288 authen = function() return eu end
292 if not util.contains(accs, user) then
294 ctx.urltoken.stok = nil
295 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
296 if not user or not util.contains(accs, user) then
299 local sid = sess or luci.sys.uniqueid(16)
301 local token = luci.sys.uniqueid(16)
302 sauth.write(sid, util.get_bytecode({
305 secret=luci.sys.uniqueid(16)
307 ctx.urltoken.stok = token
309 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
310 ctx.authsession = sid
314 luci.http.status(403, "Forbidden")
318 ctx.authsession = sess
323 if track.setgroup then
324 luci.sys.process.setgroup(track.setgroup)
327 if track.setuser then
328 luci.sys.process.setuser(track.setuser)
333 if type(c.target) == "function" then
335 elseif type(c.target) == "table" then
336 target = c.target.target
340 if c and (c.index or type(target) == "function") then
342 ctx.requested = ctx.requested or ctx.dispatched
345 if c and c.index then
346 local tpl = require "luci.template"
348 if util.copcall(tpl.render, "indexer", {}) then
353 if type(target) == "function" then
354 util.copcall(function()
355 local oldenv = getfenv(target)
356 local module = require(c.module)
357 local env = setmetatable({}, {__index=
360 return rawget(tbl, key) or module[key] or oldenv[key]
366 if type(c.target) == "table" then
367 target(c.target, unpack(args))
376 --- Generate the dispatching index using the best possible strategy.
377 function createindex()
378 local path = luci.util.libpath() .. "/controller/"
379 local suff = { ".lua", ".lua.gz" }
381 if luci.util.copcall(require, "luci.fastindex") then
382 createindex_fastindex(path, suff)
384 createindex_plain(path, suff)
388 --- Generate the dispatching index using the fastindex C-indexer.
389 -- @param path Controller base directory
390 -- @param suffixes Controller file suffixes
391 function createindex_fastindex(path, suffixes)
395 fi = luci.fastindex.new("index")
396 for _, suffix in ipairs(suffixes) do
397 fi.add(path .. "*" .. suffix)
398 fi.add(path .. "*/*" .. suffix)
403 for k, v in pairs(fi.indexes) do
408 --- Generate the dispatching index using the native file-cache based strategy.
409 -- @param path Controller base directory
410 -- @param suffixes Controller file suffixes
411 function createindex_plain(path, suffixes)
412 local controllers = { }
413 for _, suffix in ipairs(suffixes) do
414 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
415 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
419 local cachedate = fs.stat(indexcache, "mtime")
422 for _, obj in ipairs(controllers) do
423 local omtime = fs.stat(path .. "/" .. obj, "mtime")
424 realdate = (omtime and omtime > realdate) and omtime or realdate
427 if cachedate > realdate then
429 sys.process.info("uid") == fs.stat(indexcache, "uid")
430 and fs.stat(indexcache, "modestr") == "rw-------",
431 "Fatal: Indexcache is not sane!"
434 index = loadfile(indexcache)()
442 for i,c in ipairs(controllers) do
443 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
444 for _, suffix in ipairs(suffixes) do
445 module = module:gsub(suffix.."$", "")
448 local mod = require(module)
449 local idx = mod.index
451 if type(idx) == "function" then
457 local f = nixio.open(indexcache, "w", 600)
458 f:writeall(util.get_bytecode(index))
463 --- Create the dispatching tree from the index.
464 -- Build the index before if it does not exist yet.
465 function createtree()
471 local tree = {nodes={}}
474 ctx.treecache = setmetatable({}, {__mode="v"})
478 -- Load default translation
479 require "luci.i18n".loadc("base")
481 local scope = setmetatable({}, {__index = luci.dispatcher})
483 for k, v in pairs(index) do
489 local function modisort(a,b)
490 return modi[a].order < modi[b].order
493 for _, v in util.spairs(modi, modisort) do
494 scope._NAME = v.module
495 setfenv(v.func, scope)
502 --- Register a tree modifier.
503 -- @param func Modifier function
504 -- @param order Modifier order value (optional)
505 function modifier(func, order)
506 context.modifiers[#context.modifiers+1] = {
514 --- Clone a node of the dispatching tree to another position.
515 -- @param path Virtual path destination
516 -- @param clone Virtual path source
517 -- @param title Destination node title (optional)
518 -- @param order Destination node order value (optional)
519 -- @return Dispatching tree node
520 function assign(path, clone, title, order)
521 local obj = node(unpack(path))
528 setmetatable(obj, {__index = _create_node(clone)})
533 --- Create a new dispatching node and define common parameters.
534 -- @param path Virtual path
535 -- @param target Target function to call when dispatched.
536 -- @param title Destination node title
537 -- @param order Destination node order value (optional)
538 -- @return Dispatching tree node
539 function entry(path, target, title, order)
540 local c = node(unpack(path))
545 c.module = getfenv(2)._NAME
550 --- Fetch or create a dispatching node without setting the target module or
551 -- enabling the node.
552 -- @param ... Virtual path
553 -- @return Dispatching tree node
555 return _create_node({...})
558 --- Fetch or create a new dispatching node.
559 -- @param ... Virtual path
560 -- @return Dispatching tree node
562 local c = _create_node({...})
564 c.module = getfenv(2)._NAME
570 function _create_node(path, cache)
575 cache = cache or context.treecache
576 local name = table.concat(path, ".")
577 local c = cache[name]
580 local new = {nodes={}, auto=true, path=util.clone(path)}
581 local last = table.remove(path)
583 c = _create_node(path, cache)
596 --- Create a redirect to another dispatching node.
597 -- @param ... Virtual path destination
601 for _, r in ipairs({...}) do
609 --- Rewrite the first x path values of the request.
610 -- @param n Number of path values to replace
611 -- @param ... Virtual path to replace removed path values with
612 function rewrite(n, ...)
615 local dispatched = util.clone(context.dispatched)
618 table.remove(dispatched, 1)
621 for i, r in ipairs(req) do
622 table.insert(dispatched, i, r)
625 for _, r in ipairs({...}) do
626 dispatched[#dispatched+1] = r
634 local function _call(self, ...)
635 if #self.argv > 0 then
636 return getfenv()[self.name](unpack(self.argv), ...)
638 return getfenv()[self.name](...)
642 --- Create a function-call dispatching target.
643 -- @param name Target function of local controller
644 -- @param ... Additional parameters passed to the function
645 function call(name, ...)
646 return {type = "call", argv = {...}, name = name, target = _call}
650 local _template = function(self, ...)
651 require "luci.template".render(self.view)
654 --- Create a template render dispatching target.
655 -- @param name Template to be rendered
656 function template(name)
657 return {type = "template", view = name, target = _template}
661 local function _cbi(self, ...)
662 local cbi = require "luci.cbi"
663 local tpl = require "luci.template"
664 local http = require "luci.http"
666 local config = self.config or {}
667 local maps = cbi.load(self.model, ...)
671 for i, res in ipairs(maps) do
673 local cstate = res:parse()
674 if cstate and (not state or cstate < state) then
679 local function _resolve_path(path)
680 return type(path) == "table" and build_url(unpack(path)) or path
683 if config.on_valid_to and state and state > 0 and state < 2 then
684 http.redirect(_resolve_path(config.on_valid_to))
688 if config.on_changed_to and state and state > 1 then
689 http.redirect(_resolve_path(config.on_changed_to))
693 if config.on_success_to and state and state > 0 then
694 http.redirect(_resolve_path(config.on_success_to))
698 if config.state_handler then
699 if not config.state_handler(state, maps) then
704 local pageaction = true
705 http.header("X-CBI-State", state or 0)
706 if not config.noheader then
707 tpl.render("cbi/header", {state = state})
709 for i, res in ipairs(maps) do
711 if res.pageaction == false then
715 if not config.nofooter then
716 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
720 --- Create a CBI model dispatching target.
721 -- @param model CBI model to be rendered
722 function cbi(model, config)
723 return {type = "cbi", config = config, model = model, target = _cbi}
727 local function _arcombine(self, ...)
729 local target = #argv > 0 and self.targets[2] or self.targets[1]
730 setfenv(target.target, self.env)
731 target:target(unpack(argv))
734 --- Create a combined dispatching target for non argv and argv requests.
735 -- @param trg1 Overview Target
736 -- @param trg2 Detail Target
737 function arcombine(trg1, trg2)
738 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
742 local function _form(self, ...)
743 local cbi = require "luci.cbi"
744 local tpl = require "luci.template"
745 local http = require "luci.http"
747 local maps = luci.cbi.load(self.model, ...)
750 for i, res in ipairs(maps) do
751 local cstate = res:parse()
752 if cstate and (not state or cstate < state) then
757 http.header("X-CBI-State", state or 0)
759 for i, res in ipairs(maps) do
765 --- Create a CBI form model dispatching target.
766 -- @param model CBI form model tpo be rendered
768 return {type = "cbi", model = model, target = _form}