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)
112 luci.http.context.request = request
114 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
116 for node in pathinfo:gmatch("[^/]+") do
117 table.insert(context.request, node)
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
126 --context._disable_memtrace()
129 --- Dispatches a LuCI virtual path.
130 -- @param request Virtual path
131 function dispatch(request)
132 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
135 ctx.urltoken = ctx.urltoken or {}
137 local conf = require "luci.config"
139 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
141 local lang = conf.main.lang
142 if lang == "auto" then
143 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
144 for lpat in aclang:gmatch("[%w-]+") do
145 lpat = lpat and lpat:gsub("-", "_")
146 if conf.languages[lpat] then
152 require "luci.i18n".setlanguage(lang)
163 ctx.requestargs = ctx.requestargs or args
166 local token = ctx.urltoken
170 for i, s in ipairs(request) do
173 tkey, tval = s:match(";(%w+)=(.*)")
188 util.update(track, c)
197 for j=n+1, #request do
198 args[#args+1] = request[j]
199 freq[#freq+1] = request[j]
203 ctx.requestpath = freq
207 require("luci.i18n").loadc(track.i18n)
210 -- Init template engine
211 if (c and c.index) or not track.notemplate then
212 local tpl = require("luci.template")
213 local media = track.mediaurlbase or luci.config.main.mediaurlbase
214 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
215 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
217 for name, theme in pairs(luci.config.themes) do
218 if name:sub(1,1) ~= "." and pcall(tpl.Template,
219 "themes/%s/header" % fs.basename(theme)) then
223 assert(media, "No valid theme found")
226 tpl.context.viewns = setmetatable({
227 write = luci.http.write;
228 include = function(name) tpl.Template(name):render(getfenv(2)) end;
229 translate = function(...) return require("luci.i18n").translate(...) end;
230 striptags = util.striptags;
232 theme = fs.basename(media);
233 resource = luci.config.main.resourcebase
234 }, {__index=function(table, key)
235 if key == "controller" then
237 elseif key == "REQUEST_URI" then
238 return build_url(unpack(ctx.requestpath))
240 return rawget(table, key) or _G[key]
245 track.dependent = (track.dependent ~= false)
246 assert(not track.dependent or not track.auto, "Access Violation")
248 if track.sysauth then
249 local sauth = require "luci.sauth"
251 local authen = type(track.sysauth_authenticator) == "function"
252 and track.sysauth_authenticator
253 or authenticator[track.sysauth_authenticator]
255 local def = (type(track.sysauth) == "string") and track.sysauth
256 local accs = def and {track.sysauth} or track.sysauth
257 local sess = ctx.authsession
258 local verifytoken = false
260 sess = luci.http.getcookie("sysauth")
261 sess = sess and sess:match("^[a-f0-9]+$")
265 local sdat = sauth.read(sess)
269 sdat = loadstring(sdat)
272 if not verifytoken or ctx.urltoken.stok == sdat.token then
277 if not util.contains(accs, user) then
279 ctx.urltoken.stok = nil
280 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
281 if not user or not util.contains(accs, user) then
284 local sid = sess or luci.sys.uniqueid(16)
286 local token = luci.sys.uniqueid(16)
287 sauth.write(sid, util.get_bytecode({
290 secret=luci.sys.uniqueid(16)
292 ctx.urltoken.stok = token
294 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
295 ctx.authsession = sid
298 luci.http.status(403, "Forbidden")
302 ctx.authsession = sess
306 if track.setgroup then
307 luci.sys.process.setgroup(track.setgroup)
310 if track.setuser then
311 luci.sys.process.setuser(track.setuser)
316 if type(c.target) == "function" then
318 elseif type(c.target) == "table" then
319 target = c.target.target
323 if c and (c.index or type(target) == "function") then
325 ctx.requested = ctx.requested or ctx.dispatched
328 if c and c.index then
329 local tpl = require "luci.template"
331 if util.copcall(tpl.render, "indexer", {}) then
336 if type(target) == "function" then
337 util.copcall(function()
338 local oldenv = getfenv(target)
339 local module = require(c.module)
340 local env = setmetatable({}, {__index=
343 return rawget(tbl, key) or module[key] or oldenv[key]
349 if type(c.target) == "table" then
350 target(c.target, unpack(args))
359 --- Generate the dispatching index using the best possible strategy.
360 function createindex()
361 local path = luci.util.libpath() .. "/controller/"
362 local suff = { ".lua", ".lua.gz" }
364 if luci.util.copcall(require, "luci.fastindex") then
365 createindex_fastindex(path, suff)
367 createindex_plain(path, suff)
371 --- Generate the dispatching index using the fastindex C-indexer.
372 -- @param path Controller base directory
373 -- @param suffixes Controller file suffixes
374 function createindex_fastindex(path, suffixes)
378 fi = luci.fastindex.new("index")
379 for _, suffix in ipairs(suffixes) do
380 fi.add(path .. "*" .. suffix)
381 fi.add(path .. "*/*" .. suffix)
386 for k, v in pairs(fi.indexes) do
391 --- Generate the dispatching index using the native file-cache based strategy.
392 -- @param path Controller base directory
393 -- @param suffixes Controller file suffixes
394 function createindex_plain(path, suffixes)
395 local controllers = { }
396 for _, suffix in ipairs(suffixes) do
397 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
398 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
402 local cachedate = fs.stat(indexcache, "mtime")
405 for _, obj in ipairs(controllers) do
406 local omtime = fs.stat(path .. "/" .. obj, "mtime")
407 realdate = (omtime and omtime > realdate) and omtime or realdate
410 if cachedate > realdate then
412 sys.process.info("uid") == fs.stat(indexcache, "uid")
413 and fs.stat(indexcache, "modestr") == "rw-------",
414 "Fatal: Indexcache is not sane!"
417 index = loadfile(indexcache)()
425 for i,c in ipairs(controllers) do
426 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
427 for _, suffix in ipairs(suffixes) do
428 module = module:gsub(suffix.."$", "")
431 local mod = require(module)
432 local idx = mod.index
434 if type(idx) == "function" then
440 local f = nixio.open(indexcache, "w", 600)
441 f:writeall(util.get_bytecode(index))
446 --- Create the dispatching tree from the index.
447 -- Build the index before if it does not exist yet.
448 function createtree()
454 local tree = {nodes={}}
457 ctx.treecache = setmetatable({}, {__mode="v"})
461 -- Load default translation
462 require "luci.i18n".loadc("default")
464 local scope = setmetatable({}, {__index = luci.dispatcher})
466 for k, v in pairs(index) do
472 local function modisort(a,b)
473 return modi[a].order < modi[b].order
476 for _, v in util.spairs(modi, modisort) do
477 scope._NAME = v.module
478 setfenv(v.func, scope)
485 --- Register a tree modifier.
486 -- @param func Modifier function
487 -- @param order Modifier order value (optional)
488 function modifier(func, order)
489 context.modifiers[#context.modifiers+1] = {
497 --- Clone a node of the dispatching tree to another position.
498 -- @param path Virtual path destination
499 -- @param clone Virtual path source
500 -- @param title Destination node title (optional)
501 -- @param order Destination node order value (optional)
502 -- @return Dispatching tree node
503 function assign(path, clone, title, order)
504 local obj = node(unpack(path))
511 setmetatable(obj, {__index = _create_node(clone)})
516 --- Create a new dispatching node and define common parameters.
517 -- @param path Virtual path
518 -- @param target Target function to call when dispatched.
519 -- @param title Destination node title
520 -- @param order Destination node order value (optional)
521 -- @return Dispatching tree node
522 function entry(path, target, title, order)
523 local c = node(unpack(path))
528 c.module = getfenv(2)._NAME
533 --- Fetch or create a dispatching node without setting the target module or
534 -- enabling the node.
535 -- @param ... Virtual path
536 -- @return Dispatching tree node
538 return _create_node({...})
541 --- Fetch or create a new dispatching node.
542 -- @param ... Virtual path
543 -- @return Dispatching tree node
545 local c = _create_node({...})
547 c.module = getfenv(2)._NAME
553 function _create_node(path, cache)
558 cache = cache or context.treecache
559 local name = table.concat(path, ".")
560 local c = cache[name]
563 local new = {nodes={}, auto=true, path=util.clone(path)}
564 local last = table.remove(path)
566 c = _create_node(path, cache)
579 --- Create a redirect to another dispatching node.
580 -- @param ... Virtual path destination
584 for _, r in ipairs({...}) do
592 --- Rewrite the first x path values of the request.
593 -- @param n Number of path values to replace
594 -- @param ... Virtual path to replace removed path values with
595 function rewrite(n, ...)
598 local dispatched = util.clone(context.dispatched)
601 table.remove(dispatched, 1)
604 for i, r in ipairs(req) do
605 table.insert(dispatched, i, r)
608 for _, r in ipairs({...}) do
609 dispatched[#dispatched+1] = r
617 local function _call(self, ...)
618 if #self.argv > 0 then
619 return getfenv()[self.name](unpack(self.argv), ...)
621 return getfenv()[self.name](...)
625 --- Create a function-call dispatching target.
626 -- @param name Target function of local controller
627 -- @param ... Additional parameters passed to the function
628 function call(name, ...)
629 return {type = "call", argv = {...}, name = name, target = _call}
633 local _template = function(self, ...)
634 require "luci.template".render(self.view)
637 --- Create a template render dispatching target.
638 -- @param name Template to be rendered
639 function template(name)
640 return {type = "template", view = name, target = _template}
644 local function _cbi(self, ...)
645 local cbi = require "luci.cbi"
646 local tpl = require "luci.template"
647 local http = require "luci.http"
649 local config = self.config or {}
650 local maps = cbi.load(self.model, ...)
654 for i, res in ipairs(maps) do
656 local cstate = res:parse()
657 if cstate and (not state or cstate < state) then
662 local function _resolve_path(path)
663 return type(path) == "table" and build_url(unpack(path)) or path
666 if config.on_valid_to and state and state > 0 and state < 2 then
667 http.redirect(_resolve_path(config.on_valid_to))
671 if config.on_changed_to and state and state > 1 then
672 http.redirect(_resolve_path(config.on_changed_to))
676 if config.on_success_to and state and state > 0 then
677 http.redirect(_resolve_path(config.on_success_to))
681 if config.state_handler then
682 if not config.state_handler(state, maps) then
687 local pageaction = true
688 http.header("X-CBI-State", state or 0)
689 if not config.noheader then
690 tpl.render("cbi/header", {state = state})
692 for i, res in ipairs(maps) do
694 if res.pageaction == false then
698 if not config.nofooter then
699 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
703 --- Create a CBI model dispatching target.
704 -- @param model CBI model to be rendered
705 function cbi(model, config)
706 return {type = "cbi", config = config, model = model, target = _cbi}
710 local function _arcombine(self, ...)
712 local target = #argv > 0 and self.targets[2] or self.targets[1]
713 setfenv(target.target, self.env)
714 target:target(unpack(argv))
717 --- Create a combined dispatching target for non argv and argv requests.
718 -- @param trg1 Overview Target
719 -- @param trg2 Detail Target
720 function arcombine(trg1, trg2)
721 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
725 local function _form(self, ...)
726 local cbi = require "luci.cbi"
727 local tpl = require "luci.template"
728 local http = require "luci.http"
730 local maps = luci.cbi.load(self.model, ...)
733 for i, res in ipairs(maps) do
734 local cstate = res:parse()
735 if cstate and (not state or cstate < state) then
740 http.header("X-CBI-State", state or 0)
742 for i, res in ipairs(maps) do
748 --- Create a CBI form model dispatching target.
749 -- @param model CBI form model tpo be rendered
751 return {type = "cbi", model = model, target = _form}