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 or "auto"
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 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
241 striptags = util.striptags;
242 pcdata = util.pcdata;
244 theme = fs.basename(media);
245 resource = luci.config.main.resourcebase
246 }, {__index=function(table, key)
247 if key == "controller" then
249 elseif key == "REQUEST_URI" then
250 return build_url(unpack(ctx.requestpath))
252 return rawget(table, key) or _G[key]
257 track.dependent = (track.dependent ~= false)
258 assert(not track.dependent or not track.auto, "Access Violation")
260 if track.sysauth then
261 local sauth = require "luci.sauth"
263 local authen = type(track.sysauth_authenticator) == "function"
264 and track.sysauth_authenticator
265 or authenticator[track.sysauth_authenticator]
267 local def = (type(track.sysauth) == "string") and track.sysauth
268 local accs = def and {track.sysauth} or track.sysauth
269 local sess = ctx.authsession
270 local verifytoken = false
272 sess = luci.http.getcookie("sysauth")
273 sess = sess and sess:match("^[a-f0-9]*$")
277 local sdat = sauth.read(sess)
281 sdat = loadstring(sdat)
284 if not verifytoken or ctx.urltoken.stok == sdat.token then
288 local eu = http.getenv("HTTP_AUTH_USER")
289 local ep = http.getenv("HTTP_AUTH_PASS")
290 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
291 authen = function() return eu end
295 if not util.contains(accs, user) then
297 ctx.urltoken.stok = nil
298 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
299 if not user or not util.contains(accs, user) then
302 local sid = sess or luci.sys.uniqueid(16)
304 local token = luci.sys.uniqueid(16)
305 sauth.write(sid, util.get_bytecode({
308 secret=luci.sys.uniqueid(16)
310 ctx.urltoken.stok = token
312 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
313 ctx.authsession = sid
317 luci.http.status(403, "Forbidden")
321 ctx.authsession = sess
326 if track.setgroup then
327 luci.sys.process.setgroup(track.setgroup)
330 if track.setuser then
331 luci.sys.process.setuser(track.setuser)
336 if type(c.target) == "function" then
338 elseif type(c.target) == "table" then
339 target = c.target.target
343 if c and (c.index or type(target) == "function") then
345 ctx.requested = ctx.requested or ctx.dispatched
348 if c and c.index then
349 local tpl = require "luci.template"
351 if util.copcall(tpl.render, "indexer", {}) then
356 if type(target) == "function" then
357 util.copcall(function()
358 local oldenv = getfenv(target)
359 local module = require(c.module)
360 local env = setmetatable({}, {__index=
363 return rawget(tbl, key) or module[key] or oldenv[key]
369 if type(c.target) == "table" then
370 target(c.target, unpack(args))
379 --- Generate the dispatching index using the best possible strategy.
380 function createindex()
381 local path = luci.util.libpath() .. "/controller/"
382 local suff = { ".lua", ".lua.gz" }
384 if luci.util.copcall(require, "luci.fastindex") then
385 createindex_fastindex(path, suff)
387 createindex_plain(path, suff)
391 --- Generate the dispatching index using the fastindex C-indexer.
392 -- @param path Controller base directory
393 -- @param suffixes Controller file suffixes
394 function createindex_fastindex(path, suffixes)
398 fi = luci.fastindex.new("index")
399 for _, suffix in ipairs(suffixes) do
400 fi.add(path .. "*" .. suffix)
401 fi.add(path .. "*/*" .. suffix)
406 for k, v in pairs(fi.indexes) do
411 --- Generate the dispatching index using the native file-cache based strategy.
412 -- @param path Controller base directory
413 -- @param suffixes Controller file suffixes
414 function createindex_plain(path, suffixes)
415 local controllers = { }
416 for _, suffix in ipairs(suffixes) do
417 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
418 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
422 local cachedate = fs.stat(indexcache, "mtime")
425 for _, obj in ipairs(controllers) do
426 local omtime = fs.stat(path .. "/" .. obj, "mtime")
427 realdate = (omtime and omtime > realdate) and omtime or realdate
430 if cachedate > realdate then
432 sys.process.info("uid") == fs.stat(indexcache, "uid")
433 and fs.stat(indexcache, "modestr") == "rw-------",
434 "Fatal: Indexcache is not sane!"
437 index = loadfile(indexcache)()
445 for i,c in ipairs(controllers) do
446 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
447 for _, suffix in ipairs(suffixes) do
448 module = module:gsub(suffix.."$", "")
451 local mod = require(module)
452 local idx = mod.index
454 if type(idx) == "function" then
460 local f = nixio.open(indexcache, "w", 600)
461 f:writeall(util.get_bytecode(index))
466 --- Create the dispatching tree from the index.
467 -- Build the index before if it does not exist yet.
468 function createtree()
474 local tree = {nodes={}}
477 ctx.treecache = setmetatable({}, {__mode="v"})
481 -- Load default translation
482 require "luci.i18n".loadc("base")
484 local scope = setmetatable({}, {__index = luci.dispatcher})
486 for k, v in pairs(index) do
492 local function modisort(a,b)
493 return modi[a].order < modi[b].order
496 for _, v in util.spairs(modi, modisort) do
497 scope._NAME = v.module
498 setfenv(v.func, scope)
505 --- Register a tree modifier.
506 -- @param func Modifier function
507 -- @param order Modifier order value (optional)
508 function modifier(func, order)
509 context.modifiers[#context.modifiers+1] = {
517 --- Clone a node of the dispatching tree to another position.
518 -- @param path Virtual path destination
519 -- @param clone Virtual path source
520 -- @param title Destination node title (optional)
521 -- @param order Destination node order value (optional)
522 -- @return Dispatching tree node
523 function assign(path, clone, title, order)
524 local obj = node(unpack(path))
531 setmetatable(obj, {__index = _create_node(clone)})
536 --- Create a new dispatching node and define common parameters.
537 -- @param path Virtual path
538 -- @param target Target function to call when dispatched.
539 -- @param title Destination node title
540 -- @param order Destination node order value (optional)
541 -- @return Dispatching tree node
542 function entry(path, target, title, order)
543 local c = node(unpack(path))
548 c.module = getfenv(2)._NAME
553 --- Fetch or create a dispatching node without setting the target module or
554 -- enabling the node.
555 -- @param ... Virtual path
556 -- @return Dispatching tree node
558 return _create_node({...})
561 --- Fetch or create a new dispatching node.
562 -- @param ... Virtual path
563 -- @return Dispatching tree node
565 local c = _create_node({...})
567 c.module = getfenv(2)._NAME
573 function _create_node(path, cache)
578 cache = cache or context.treecache
579 local name = table.concat(path, ".")
580 local c = cache[name]
583 local new = {nodes={}, auto=true, path=util.clone(path)}
584 local last = table.remove(path)
586 c = _create_node(path, cache)
599 --- Create a redirect to another dispatching node.
600 -- @param ... Virtual path destination
604 for _, r in ipairs({...}) do
612 --- Rewrite the first x path values of the request.
613 -- @param n Number of path values to replace
614 -- @param ... Virtual path to replace removed path values with
615 function rewrite(n, ...)
618 local dispatched = util.clone(context.dispatched)
621 table.remove(dispatched, 1)
624 for i, r in ipairs(req) do
625 table.insert(dispatched, i, r)
628 for _, r in ipairs({...}) do
629 dispatched[#dispatched+1] = r
637 local function _call(self, ...)
638 if #self.argv > 0 then
639 return getfenv()[self.name](unpack(self.argv), ...)
641 return getfenv()[self.name](...)
645 --- Create a function-call dispatching target.
646 -- @param name Target function of local controller
647 -- @param ... Additional parameters passed to the function
648 function call(name, ...)
649 return {type = "call", argv = {...}, name = name, target = _call}
653 local _template = function(self, ...)
654 require "luci.template".render(self.view)
657 --- Create a template render dispatching target.
658 -- @param name Template to be rendered
659 function template(name)
660 return {type = "template", view = name, target = _template}
664 local function _cbi(self, ...)
665 local cbi = require "luci.cbi"
666 local tpl = require "luci.template"
667 local http = require "luci.http"
669 local config = self.config or {}
670 local maps = cbi.load(self.model, ...)
674 for i, res in ipairs(maps) do
676 local cstate = res:parse()
677 if cstate and (not state or cstate < state) then
682 local function _resolve_path(path)
683 return type(path) == "table" and build_url(unpack(path)) or path
686 if config.on_valid_to and state and state > 0 and state < 2 then
687 http.redirect(_resolve_path(config.on_valid_to))
691 if config.on_changed_to and state and state > 1 then
692 http.redirect(_resolve_path(config.on_changed_to))
696 if config.on_success_to and state and state > 0 then
697 http.redirect(_resolve_path(config.on_success_to))
701 if config.state_handler then
702 if not config.state_handler(state, maps) then
707 local pageaction = true
708 http.header("X-CBI-State", state or 0)
709 if not config.noheader then
710 tpl.render("cbi/header", {state = state})
712 for i, res in ipairs(maps) do
714 if res.pageaction == false then
718 if not config.nofooter then
719 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
723 --- Create a CBI model dispatching target.
724 -- @param model CBI model to be rendered
725 function cbi(model, config)
726 return {type = "cbi", config = config, model = model, target = _cbi}
730 local function _arcombine(self, ...)
732 local target = #argv > 0 and self.targets[2] or self.targets[1]
733 setfenv(target.target, self.env)
734 target:target(unpack(argv))
737 --- Create a combined dispatching target for non argv and argv requests.
738 -- @param trg1 Overview Target
739 -- @param trg2 Detail Target
740 function arcombine(trg1, trg2)
741 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
745 local function _form(self, ...)
746 local cbi = require "luci.cbi"
747 local tpl = require "luci.template"
748 local http = require "luci.http"
750 local maps = luci.cbi.load(self.model, ...)
753 for i, res in ipairs(maps) do
754 local cstate = res:parse()
755 if cstate and (not state or cstate < state) then
760 http.header("X-CBI-State", state or 0)
762 for i, res in ipairs(maps) do
768 --- Create a CBI form model dispatching target.
769 -- @param model CBI form model tpo be rendered
771 return {type = "cbi", model = model, target = _form}