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;
239 theme = fs.basename(media);
240 resource = luci.config.main.resourcebase
241 }, {__index=function(table, key)
242 if key == "controller" then
244 elseif key == "REQUEST_URI" then
245 return build_url(unpack(ctx.requestpath))
247 return rawget(table, key) or _G[key]
252 track.dependent = (track.dependent ~= false)
253 assert(not track.dependent or not track.auto, "Access Violation")
255 if track.sysauth then
256 local sauth = require "luci.sauth"
258 local authen = type(track.sysauth_authenticator) == "function"
259 and track.sysauth_authenticator
260 or authenticator[track.sysauth_authenticator]
262 local def = (type(track.sysauth) == "string") and track.sysauth
263 local accs = def and {track.sysauth} or track.sysauth
264 local sess = ctx.authsession
265 local verifytoken = false
267 sess = luci.http.getcookie("sysauth")
268 sess = sess and sess:match("^[a-f0-9]*$")
272 local sdat = sauth.read(sess)
276 sdat = loadstring(sdat)
279 if not verifytoken or ctx.urltoken.stok == sdat.token then
283 local eu = http.getenv("HTTP_AUTH_USER")
284 local ep = http.getenv("HTTP_AUTH_PASS")
285 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
286 authen = function() return eu end
290 if not util.contains(accs, user) then
292 ctx.urltoken.stok = nil
293 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
294 if not user or not util.contains(accs, user) then
297 local sid = sess or luci.sys.uniqueid(16)
299 local token = luci.sys.uniqueid(16)
300 sauth.write(sid, util.get_bytecode({
303 secret=luci.sys.uniqueid(16)
305 ctx.urltoken.stok = token
307 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
308 ctx.authsession = sid
312 luci.http.status(403, "Forbidden")
316 ctx.authsession = sess
321 if track.setgroup then
322 luci.sys.process.setgroup(track.setgroup)
325 if track.setuser then
326 luci.sys.process.setuser(track.setuser)
331 if type(c.target) == "function" then
333 elseif type(c.target) == "table" then
334 target = c.target.target
338 if c and (c.index or type(target) == "function") then
340 ctx.requested = ctx.requested or ctx.dispatched
343 if c and c.index then
344 local tpl = require "luci.template"
346 if util.copcall(tpl.render, "indexer", {}) then
351 if type(target) == "function" then
352 util.copcall(function()
353 local oldenv = getfenv(target)
354 local module = require(c.module)
355 local env = setmetatable({}, {__index=
358 return rawget(tbl, key) or module[key] or oldenv[key]
364 if type(c.target) == "table" then
365 target(c.target, unpack(args))
374 --- Generate the dispatching index using the best possible strategy.
375 function createindex()
376 local path = luci.util.libpath() .. "/controller/"
377 local suff = { ".lua", ".lua.gz" }
379 if luci.util.copcall(require, "luci.fastindex") then
380 createindex_fastindex(path, suff)
382 createindex_plain(path, suff)
386 --- Generate the dispatching index using the fastindex C-indexer.
387 -- @param path Controller base directory
388 -- @param suffixes Controller file suffixes
389 function createindex_fastindex(path, suffixes)
393 fi = luci.fastindex.new("index")
394 for _, suffix in ipairs(suffixes) do
395 fi.add(path .. "*" .. suffix)
396 fi.add(path .. "*/*" .. suffix)
401 for k, v in pairs(fi.indexes) do
406 --- Generate the dispatching index using the native file-cache based strategy.
407 -- @param path Controller base directory
408 -- @param suffixes Controller file suffixes
409 function createindex_plain(path, suffixes)
410 local controllers = { }
411 for _, suffix in ipairs(suffixes) do
412 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
413 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
417 local cachedate = fs.stat(indexcache, "mtime")
420 for _, obj in ipairs(controllers) do
421 local omtime = fs.stat(path .. "/" .. obj, "mtime")
422 realdate = (omtime and omtime > realdate) and omtime or realdate
425 if cachedate > realdate then
427 sys.process.info("uid") == fs.stat(indexcache, "uid")
428 and fs.stat(indexcache, "modestr") == "rw-------",
429 "Fatal: Indexcache is not sane!"
432 index = loadfile(indexcache)()
440 for i,c in ipairs(controllers) do
441 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
442 for _, suffix in ipairs(suffixes) do
443 module = module:gsub(suffix.."$", "")
446 local mod = require(module)
447 local idx = mod.index
449 if type(idx) == "function" then
455 local f = nixio.open(indexcache, "w", 600)
456 f:writeall(util.get_bytecode(index))
461 --- Create the dispatching tree from the index.
462 -- Build the index before if it does not exist yet.
463 function createtree()
469 local tree = {nodes={}}
472 ctx.treecache = setmetatable({}, {__mode="v"})
476 -- Load default translation
477 require "luci.i18n".loadc("default")
479 local scope = setmetatable({}, {__index = luci.dispatcher})
481 for k, v in pairs(index) do
487 local function modisort(a,b)
488 return modi[a].order < modi[b].order
491 for _, v in util.spairs(modi, modisort) do
492 scope._NAME = v.module
493 setfenv(v.func, scope)
500 --- Register a tree modifier.
501 -- @param func Modifier function
502 -- @param order Modifier order value (optional)
503 function modifier(func, order)
504 context.modifiers[#context.modifiers+1] = {
512 --- Clone a node of the dispatching tree to another position.
513 -- @param path Virtual path destination
514 -- @param clone Virtual path source
515 -- @param title Destination node title (optional)
516 -- @param order Destination node order value (optional)
517 -- @return Dispatching tree node
518 function assign(path, clone, title, order)
519 local obj = node(unpack(path))
526 setmetatable(obj, {__index = _create_node(clone)})
531 --- Create a new dispatching node and define common parameters.
532 -- @param path Virtual path
533 -- @param target Target function to call when dispatched.
534 -- @param title Destination node title
535 -- @param order Destination node order value (optional)
536 -- @return Dispatching tree node
537 function entry(path, target, title, order)
538 local c = node(unpack(path))
543 c.module = getfenv(2)._NAME
548 --- Fetch or create a dispatching node without setting the target module or
549 -- enabling the node.
550 -- @param ... Virtual path
551 -- @return Dispatching tree node
553 return _create_node({...})
556 --- Fetch or create a new dispatching node.
557 -- @param ... Virtual path
558 -- @return Dispatching tree node
560 local c = _create_node({...})
562 c.module = getfenv(2)._NAME
568 function _create_node(path, cache)
573 cache = cache or context.treecache
574 local name = table.concat(path, ".")
575 local c = cache[name]
578 local new = {nodes={}, auto=true, path=util.clone(path)}
579 local last = table.remove(path)
581 c = _create_node(path, cache)
594 --- Create a redirect to another dispatching node.
595 -- @param ... Virtual path destination
599 for _, r in ipairs({...}) do
607 --- Rewrite the first x path values of the request.
608 -- @param n Number of path values to replace
609 -- @param ... Virtual path to replace removed path values with
610 function rewrite(n, ...)
613 local dispatched = util.clone(context.dispatched)
616 table.remove(dispatched, 1)
619 for i, r in ipairs(req) do
620 table.insert(dispatched, i, r)
623 for _, r in ipairs({...}) do
624 dispatched[#dispatched+1] = r
632 local function _call(self, ...)
633 if #self.argv > 0 then
634 return getfenv()[self.name](unpack(self.argv), ...)
636 return getfenv()[self.name](...)
640 --- Create a function-call dispatching target.
641 -- @param name Target function of local controller
642 -- @param ... Additional parameters passed to the function
643 function call(name, ...)
644 return {type = "call", argv = {...}, name = name, target = _call}
648 local _template = function(self, ...)
649 require "luci.template".render(self.view)
652 --- Create a template render dispatching target.
653 -- @param name Template to be rendered
654 function template(name)
655 return {type = "template", view = name, target = _template}
659 local function _cbi(self, ...)
660 local cbi = require "luci.cbi"
661 local tpl = require "luci.template"
662 local http = require "luci.http"
664 local config = self.config or {}
665 local maps = cbi.load(self.model, ...)
669 for i, res in ipairs(maps) do
671 local cstate = res:parse()
672 if cstate and (not state or cstate < state) then
677 local function _resolve_path(path)
678 return type(path) == "table" and build_url(unpack(path)) or path
681 if config.on_valid_to and state and state > 0 and state < 2 then
682 http.redirect(_resolve_path(config.on_valid_to))
686 if config.on_changed_to and state and state > 1 then
687 http.redirect(_resolve_path(config.on_changed_to))
691 if config.on_success_to and state and state > 0 then
692 http.redirect(_resolve_path(config.on_success_to))
696 if config.state_handler then
697 if not config.state_handler(state, maps) then
702 local pageaction = true
703 http.header("X-CBI-State", state or 0)
704 if not config.noheader then
705 tpl.render("cbi/header", {state = state})
707 for i, res in ipairs(maps) do
709 if res.pageaction == false then
713 if not config.nofooter then
714 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
718 --- Create a CBI model dispatching target.
719 -- @param model CBI model to be rendered
720 function cbi(model, config)
721 return {type = "cbi", config = config, model = model, target = _cbi}
725 local function _arcombine(self, ...)
727 local target = #argv > 0 and self.targets[2] or self.targets[1]
728 setfenv(target.target, self.env)
729 target:target(unpack(argv))
732 --- Create a combined dispatching target for non argv and argv requests.
733 -- @param trg1 Overview Target
734 -- @param trg2 Detail Target
735 function arcombine(trg1, trg2)
736 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
740 local function _form(self, ...)
741 local cbi = require "luci.cbi"
742 local tpl = require "luci.template"
743 local http = require "luci.http"
745 local maps = luci.cbi.load(self.model, ...)
748 for i, res in ipairs(maps) do
749 local cstate = res:parse()
750 if cstate and (not state or cstate < state) then
755 http.header("X-CBI-State", state or 0)
757 for i, res in ipairs(maps) do
763 --- Create a CBI form model dispatching target.
764 -- @param model CBI form model tpo be rendered
766 return {type = "cbi", model = model, target = _form}