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 = ctx.requestpath or 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 pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
224 for name, theme in pairs(luci.config.themes) do
225 if name:sub(1,1) ~= "." and pcall(tpl.Template,
226 "themes/%s/header" % fs.basename(theme)) then
230 assert(media, "No valid theme found")
233 tpl.context.viewns = setmetatable({
234 write = luci.http.write;
235 include = function(name) tpl.Template(name):render(getfenv(2)) end;
236 translate = function(...) return require("luci.i18n").translate(...) end;
237 striptags = util.striptags;
238 pcdata = util.pcdata;
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
313 luci.http.status(403, "Forbidden")
317 ctx.authsession = sess
322 if track.setgroup then
323 luci.sys.process.setgroup(track.setgroup)
326 if track.setuser then
327 luci.sys.process.setuser(track.setuser)
332 if type(c.target) == "function" then
334 elseif type(c.target) == "table" then
335 target = c.target.target
339 if c and (c.index or type(target) == "function") then
341 ctx.requested = ctx.requested or ctx.dispatched
344 if c and c.index then
345 local tpl = require "luci.template"
347 if util.copcall(tpl.render, "indexer", {}) then
352 if type(target) == "function" then
353 util.copcall(function()
354 local oldenv = getfenv(target)
355 local module = require(c.module)
356 local env = setmetatable({}, {__index=
359 return rawget(tbl, key) or module[key] or oldenv[key]
365 if type(c.target) == "table" then
366 target(c.target, unpack(args))
375 --- Generate the dispatching index using the best possible strategy.
376 function createindex()
377 local path = luci.util.libpath() .. "/controller/"
378 local suff = { ".lua", ".lua.gz" }
380 if luci.util.copcall(require, "luci.fastindex") then
381 createindex_fastindex(path, suff)
383 createindex_plain(path, suff)
387 --- Generate the dispatching index using the fastindex C-indexer.
388 -- @param path Controller base directory
389 -- @param suffixes Controller file suffixes
390 function createindex_fastindex(path, suffixes)
394 fi = luci.fastindex.new("index")
395 for _, suffix in ipairs(suffixes) do
396 fi.add(path .. "*" .. suffix)
397 fi.add(path .. "*/*" .. suffix)
402 for k, v in pairs(fi.indexes) do
407 --- Generate the dispatching index using the native file-cache based strategy.
408 -- @param path Controller base directory
409 -- @param suffixes Controller file suffixes
410 function createindex_plain(path, suffixes)
411 local controllers = { }
412 for _, suffix in ipairs(suffixes) do
413 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
414 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
418 local cachedate = fs.stat(indexcache, "mtime")
421 for _, obj in ipairs(controllers) do
422 local omtime = fs.stat(path .. "/" .. obj, "mtime")
423 realdate = (omtime and omtime > realdate) and omtime or realdate
426 if cachedate > realdate then
428 sys.process.info("uid") == fs.stat(indexcache, "uid")
429 and fs.stat(indexcache, "modestr") == "rw-------",
430 "Fatal: Indexcache is not sane!"
433 index = loadfile(indexcache)()
441 for i,c in ipairs(controllers) do
442 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
443 for _, suffix in ipairs(suffixes) do
444 module = module:gsub(suffix.."$", "")
447 local mod = require(module)
448 local idx = mod.index
450 if type(idx) == "function" then
456 local f = nixio.open(indexcache, "w", 600)
457 f:writeall(util.get_bytecode(index))
462 --- Create the dispatching tree from the index.
463 -- Build the index before if it does not exist yet.
464 function createtree()
470 local tree = {nodes={}}
473 ctx.treecache = setmetatable({}, {__mode="v"})
477 -- Load default translation
478 require "luci.i18n".loadc("default")
480 local scope = setmetatable({}, {__index = luci.dispatcher})
482 for k, v in pairs(index) do
488 local function modisort(a,b)
489 return modi[a].order < modi[b].order
492 for _, v in util.spairs(modi, modisort) do
493 scope._NAME = v.module
494 setfenv(v.func, scope)
501 --- Register a tree modifier.
502 -- @param func Modifier function
503 -- @param order Modifier order value (optional)
504 function modifier(func, order)
505 context.modifiers[#context.modifiers+1] = {
513 --- Clone a node of the dispatching tree to another position.
514 -- @param path Virtual path destination
515 -- @param clone Virtual path source
516 -- @param title Destination node title (optional)
517 -- @param order Destination node order value (optional)
518 -- @return Dispatching tree node
519 function assign(path, clone, title, order)
520 local obj = node(unpack(path))
527 setmetatable(obj, {__index = _create_node(clone)})
532 --- Create a new dispatching node and define common parameters.
533 -- @param path Virtual path
534 -- @param target Target function to call when dispatched.
535 -- @param title Destination node title
536 -- @param order Destination node order value (optional)
537 -- @return Dispatching tree node
538 function entry(path, target, title, order)
539 local c = node(unpack(path))
544 c.module = getfenv(2)._NAME
549 --- Fetch or create a dispatching node without setting the target module or
550 -- enabling the node.
551 -- @param ... Virtual path
552 -- @return Dispatching tree node
554 return _create_node({...})
557 --- Fetch or create a new dispatching node.
558 -- @param ... Virtual path
559 -- @return Dispatching tree node
561 local c = _create_node({...})
563 c.module = getfenv(2)._NAME
569 function _create_node(path, cache)
574 cache = cache or context.treecache
575 local name = table.concat(path, ".")
576 local c = cache[name]
579 local new = {nodes={}, auto=true, path=util.clone(path)}
580 local last = table.remove(path)
582 c = _create_node(path, cache)
595 --- Create a redirect to another dispatching node.
596 -- @param ... Virtual path destination
600 for _, r in ipairs({...}) do
608 --- Rewrite the first x path values of the request.
609 -- @param n Number of path values to replace
610 -- @param ... Virtual path to replace removed path values with
611 function rewrite(n, ...)
614 local dispatched = util.clone(context.dispatched)
617 table.remove(dispatched, 1)
620 for i, r in ipairs(req) do
621 table.insert(dispatched, i, r)
624 for _, r in ipairs({...}) do
625 dispatched[#dispatched+1] = r
633 local function _call(self, ...)
634 if #self.argv > 0 then
635 return getfenv()[self.name](unpack(self.argv), ...)
637 return getfenv()[self.name](...)
641 --- Create a function-call dispatching target.
642 -- @param name Target function of local controller
643 -- @param ... Additional parameters passed to the function
644 function call(name, ...)
645 return {type = "call", argv = {...}, name = name, target = _call}
649 local _template = function(self, ...)
650 require "luci.template".render(self.view)
653 --- Create a template render dispatching target.
654 -- @param name Template to be rendered
655 function template(name)
656 return {type = "template", view = name, target = _template}
660 local function _cbi(self, ...)
661 local cbi = require "luci.cbi"
662 local tpl = require "luci.template"
663 local http = require "luci.http"
665 local config = self.config or {}
666 local maps = cbi.load(self.model, ...)
670 for i, res in ipairs(maps) do
672 local cstate = res:parse()
673 if cstate and (not state or cstate < state) then
678 local function _resolve_path(path)
679 return type(path) == "table" and build_url(unpack(path)) or path
682 if config.on_valid_to and state and state > 0 and state < 2 then
683 http.redirect(_resolve_path(config.on_valid_to))
687 if config.on_changed_to and state and state > 1 then
688 http.redirect(_resolve_path(config.on_changed_to))
692 if config.on_success_to and state and state > 0 then
693 http.redirect(_resolve_path(config.on_success_to))
697 if config.state_handler then
698 if not config.state_handler(state, maps) then
703 local pageaction = true
704 http.header("X-CBI-State", state or 0)
705 if not config.noheader then
706 tpl.render("cbi/header", {state = state})
708 for i, res in ipairs(maps) do
710 if res.pageaction == false then
714 if not config.nofooter then
715 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
719 --- Create a CBI model dispatching target.
720 -- @param model CBI model to be rendered
721 function cbi(model, config)
722 return {type = "cbi", config = config, model = model, target = _cbi}
726 local function _arcombine(self, ...)
728 local target = #argv > 0 and self.targets[2] or self.targets[1]
729 setfenv(target.target, self.env)
730 target:target(unpack(argv))
733 --- Create a combined dispatching target for non argv and argv requests.
734 -- @param trg1 Overview Target
735 -- @param trg2 Detail Target
736 function arcombine(trg1, trg2)
737 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
741 local function _form(self, ...)
742 local cbi = require "luci.cbi"
743 local tpl = require "luci.template"
744 local http = require "luci.http"
746 local maps = luci.cbi.load(self.model, ...)
749 for i, res in ipairs(maps) do
750 local cstate = res:parse()
751 if cstate and (not state or cstate < state) then
756 http.header("X-CBI-State", state or 0)
758 for i, res in ipairs(maps) do
764 --- Create a CBI form model dispatching target.
765 -- @param model CBI form model tpo be rendered
767 return {type = "cbi", model = model, target = _form}