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 url = { http.getenv("SCRIPT_NAME") or "" }
58 for k, v in pairs(context.urltoken) do
60 url[#url+1] = http.urlencode(k)
62 url[#url+1] = http.urlencode(v)
66 for _, p in ipairs(path) do
67 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
73 return table.concat(url, "")
76 --- Send a 404 error code and render the "error404" template if available.
77 -- @param message Custom error message (optional)
79 function error404(message)
80 luci.http.status(404, "Not Found")
81 message = message or "Not Found"
83 require("luci.template")
84 if not luci.util.copcall(luci.template.render, "error404") then
85 luci.http.prepare_content("text/plain")
86 luci.http.write(message)
91 --- Send a 500 error code and render the "error500" template if available.
92 -- @param message Custom error message (optional)#
94 function error500(message)
95 luci.util.perror(message)
96 if not context.template_header_sent then
97 luci.http.status(500, "Internal Server Error")
98 luci.http.prepare_content("text/plain")
99 luci.http.write(message)
101 require("luci.template")
102 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
103 luci.http.prepare_content("text/plain")
104 luci.http.write(message)
110 function authenticator.htmlauth(validator, accs, default)
111 local user = luci.http.formvalue("username")
112 local pass = luci.http.formvalue("password")
114 if user and validator(user, pass) then
119 require("luci.template")
121 luci.template.render("sysauth", {duser=default, fuser=user})
126 --- Dispatch an HTTP request.
127 -- @param request LuCI HTTP Request object
128 function httpdispatch(request, prefix)
129 luci.http.context.request = request
133 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
136 for _, node in ipairs(prefix) do
141 for node in pathinfo:gmatch("[^/]+") do
145 local stat, err = util.coxpcall(function()
146 dispatch(context.request)
151 --context._disable_memtrace()
154 --- Dispatches a LuCI virtual path.
155 -- @param request Virtual path
156 function dispatch(request)
157 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
160 ctx.urltoken = ctx.urltoken or {}
162 local conf = require "luci.config"
164 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
166 local lang = conf.main.lang or "auto"
167 if lang == "auto" then
168 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
169 for lpat in aclang:gmatch("[%w-]+") do
170 lpat = lpat and lpat:gsub("-", "_")
171 if conf.languages[lpat] then
177 require "luci.i18n".setlanguage(lang)
188 ctx.requestargs = ctx.requestargs or args
191 local token = ctx.urltoken
195 for i, s in ipairs(request) do
198 tkey, tval = s:match(";(%w+)=([a-fA-F0-9]*)")
213 util.update(track, c)
222 for j=n+1, #request do
223 args[#args+1] = request[j]
224 freq[#freq+1] = request[j]
228 ctx.requestpath = ctx.requestpath or freq
232 require("luci.i18n").loadc(track.i18n)
235 -- Init template engine
236 if (c and c.index) or not track.notemplate then
237 local tpl = require("luci.template")
238 local media = track.mediaurlbase or luci.config.main.mediaurlbase
239 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
241 for name, theme in pairs(luci.config.themes) do
242 if name:sub(1,1) ~= "." and pcall(tpl.Template,
243 "themes/%s/header" % fs.basename(theme)) then
247 assert(media, "No valid theme found")
250 tpl.context.viewns = setmetatable({
251 write = luci.http.write;
252 include = function(name) tpl.Template(name):render(getfenv(2)) end;
253 translate = function(...) return require("luci.i18n").translate(...) end;
254 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
255 striptags = util.striptags;
256 pcdata = util.pcdata;
258 theme = fs.basename(media);
259 resource = luci.config.main.resourcebase
260 }, {__index=function(table, key)
261 if key == "controller" then
263 elseif key == "REQUEST_URI" then
264 return build_url(unpack(ctx.requestpath))
266 return rawget(table, key) or _G[key]
271 track.dependent = (track.dependent ~= false)
272 assert(not track.dependent or not track.auto,
273 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
274 "has no parent node so the access to this location has been denied.\n" ..
275 "This is a software bug, please report this message at " ..
276 "http://luci.subsignal.org/trac/newticket"
279 if track.sysauth then
280 local sauth = require "luci.sauth"
282 local authen = type(track.sysauth_authenticator) == "function"
283 and track.sysauth_authenticator
284 or authenticator[track.sysauth_authenticator]
286 local def = (type(track.sysauth) == "string") and track.sysauth
287 local accs = def and {track.sysauth} or track.sysauth
288 local sess = ctx.authsession
289 local verifytoken = false
291 sess = luci.http.getcookie("sysauth")
292 sess = sess and sess:match("^[a-f0-9]*$")
296 local sdat = sauth.read(sess)
300 sdat = loadstring(sdat)
303 if not verifytoken or ctx.urltoken.stok == sdat.token then
307 local eu = http.getenv("HTTP_AUTH_USER")
308 local ep = http.getenv("HTTP_AUTH_PASS")
309 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
310 authen = function() return eu end
314 if not util.contains(accs, user) then
316 ctx.urltoken.stok = nil
317 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
318 if not user or not util.contains(accs, user) then
321 local sid = sess or luci.sys.uniqueid(16)
323 local token = luci.sys.uniqueid(16)
324 sauth.write(sid, util.get_bytecode({
327 secret=luci.sys.uniqueid(16)
329 ctx.urltoken.stok = token
331 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
332 ctx.authsession = sid
336 luci.http.status(403, "Forbidden")
340 ctx.authsession = sess
345 if track.setgroup then
346 luci.sys.process.setgroup(track.setgroup)
349 if track.setuser then
350 luci.sys.process.setuser(track.setuser)
355 if type(c.target) == "function" then
357 elseif type(c.target) == "table" then
358 target = c.target.target
362 if c and (c.index or type(target) == "function") then
364 ctx.requested = ctx.requested or ctx.dispatched
367 if c and c.index then
368 local tpl = require "luci.template"
370 if util.copcall(tpl.render, "indexer", {}) then
375 if type(target) == "function" then
376 util.copcall(function()
377 local oldenv = getfenv(target)
378 local module = require(c.module)
379 local env = setmetatable({}, {__index=
382 return rawget(tbl, key) or module[key] or oldenv[key]
388 if type(c.target) == "table" then
389 target(c.target, unpack(args))
395 if not root or not root.target then
396 error404("No root node was registered, this usually happens if no module was installed.\n" ..
397 "Install luci-admin-full and retry. " ..
398 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
400 error404("No page is registered at '" .. table.concat(request, "/") .. "/'.\n" ..
401 "If this url belongs to an extension, make sure it is properly installed.\n" ..
402 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
407 --- Generate the dispatching index using the best possible strategy.
408 function createindex()
409 local path = luci.util.libpath() .. "/controller/"
410 local suff = { ".lua", ".lua.gz" }
412 if luci.util.copcall(require, "luci.fastindex") then
413 createindex_fastindex(path, suff)
415 createindex_plain(path, suff)
419 --- Generate the dispatching index using the fastindex C-indexer.
420 -- @param path Controller base directory
421 -- @param suffixes Controller file suffixes
422 function createindex_fastindex(path, suffixes)
426 fi = luci.fastindex.new("index")
427 for _, suffix in ipairs(suffixes) do
428 fi.add(path .. "*" .. suffix)
429 fi.add(path .. "*/*" .. suffix)
434 for k, v in pairs(fi.indexes) do
439 --- Generate the dispatching index using the native file-cache based strategy.
440 -- @param path Controller base directory
441 -- @param suffixes Controller file suffixes
442 function createindex_plain(path, suffixes)
443 local controllers = { }
444 for _, suffix in ipairs(suffixes) do
445 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
446 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
450 local cachedate = fs.stat(indexcache, "mtime")
453 for _, obj in ipairs(controllers) do
454 local omtime = fs.stat(obj, "mtime")
455 realdate = (omtime and omtime > realdate) and omtime or realdate
458 if cachedate > realdate then
460 sys.process.info("uid") == fs.stat(indexcache, "uid")
461 and fs.stat(indexcache, "modestr") == "rw-------",
462 "Fatal: Indexcache is not sane!"
465 index = loadfile(indexcache)()
473 for i,c in ipairs(controllers) do
474 local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
475 for _, suffix in ipairs(suffixes) do
476 modname = modname:gsub(suffix.."$", "")
479 local mod = require(modname)
480 local idx = mod.index
482 if type(idx) == "function" then
488 local f = nixio.open(indexcache, "w", 600)
489 f:writeall(util.get_bytecode(index))
494 --- Create the dispatching tree from the index.
495 -- Build the index before if it does not exist yet.
496 function createtree()
502 local tree = {nodes={}}
505 ctx.treecache = setmetatable({}, {__mode="v"})
509 -- Load default translation
510 require "luci.i18n".loadc("base")
512 local scope = setmetatable({}, {__index = luci.dispatcher})
514 for k, v in pairs(index) do
520 local function modisort(a,b)
521 return modi[a].order < modi[b].order
524 for _, v in util.spairs(modi, modisort) do
525 scope._NAME = v.module
526 setfenv(v.func, scope)
533 --- Register a tree modifier.
534 -- @param func Modifier function
535 -- @param order Modifier order value (optional)
536 function modifier(func, order)
537 context.modifiers[#context.modifiers+1] = {
545 --- Clone a node of the dispatching tree to another position.
546 -- @param path Virtual path destination
547 -- @param clone Virtual path source
548 -- @param title Destination node title (optional)
549 -- @param order Destination node order value (optional)
550 -- @return Dispatching tree node
551 function assign(path, clone, title, order)
552 local obj = node(unpack(path))
559 setmetatable(obj, {__index = _create_node(clone)})
564 --- Create a new dispatching node and define common parameters.
565 -- @param path Virtual path
566 -- @param target Target function to call when dispatched.
567 -- @param title Destination node title
568 -- @param order Destination node order value (optional)
569 -- @return Dispatching tree node
570 function entry(path, target, title, order)
571 local c = node(unpack(path))
576 c.module = getfenv(2)._NAME
581 --- Fetch or create a dispatching node without setting the target module or
582 -- enabling the node.
583 -- @param ... Virtual path
584 -- @return Dispatching tree node
586 return _create_node({...})
589 --- Fetch or create a new dispatching node.
590 -- @param ... Virtual path
591 -- @return Dispatching tree node
593 local c = _create_node({...})
595 c.module = getfenv(2)._NAME
601 function _create_node(path, cache)
606 cache = cache or context.treecache
607 local name = table.concat(path, ".")
608 local c = cache[name]
611 local new = {nodes={}, auto=true, path=util.clone(path)}
612 local last = table.remove(path)
614 c = _create_node(path, cache)
627 --- Create a redirect to another dispatching node.
628 -- @param ... Virtual path destination
632 for _, r in ipairs({...}) do
640 --- Rewrite the first x path values of the request.
641 -- @param n Number of path values to replace
642 -- @param ... Virtual path to replace removed path values with
643 function rewrite(n, ...)
646 local dispatched = util.clone(context.dispatched)
649 table.remove(dispatched, 1)
652 for i, r in ipairs(req) do
653 table.insert(dispatched, i, r)
656 for _, r in ipairs({...}) do
657 dispatched[#dispatched+1] = r
665 local function _call(self, ...)
666 if #self.argv > 0 then
667 return getfenv()[self.name](unpack(self.argv), ...)
669 return getfenv()[self.name](...)
673 --- Create a function-call dispatching target.
674 -- @param name Target function of local controller
675 -- @param ... Additional parameters passed to the function
676 function call(name, ...)
677 return {type = "call", argv = {...}, name = name, target = _call}
681 local _template = function(self, ...)
682 require "luci.template".render(self.view)
685 --- Create a template render dispatching target.
686 -- @param name Template to be rendered
687 function template(name)
688 return {type = "template", view = name, target = _template}
692 local function _cbi(self, ...)
693 local cbi = require "luci.cbi"
694 local tpl = require "luci.template"
695 local http = require "luci.http"
697 local config = self.config or {}
698 local maps = cbi.load(self.model, ...)
702 for i, res in ipairs(maps) do
704 local cstate = res:parse()
705 if cstate and (not state or cstate < state) then
710 local function _resolve_path(path)
711 return type(path) == "table" and build_url(unpack(path)) or path
714 if config.on_valid_to and state and state > 0 and state < 2 then
715 http.redirect(_resolve_path(config.on_valid_to))
719 if config.on_changed_to and state and state > 1 then
720 http.redirect(_resolve_path(config.on_changed_to))
724 if config.on_success_to and state and state > 0 then
725 http.redirect(_resolve_path(config.on_success_to))
729 if config.state_handler then
730 if not config.state_handler(state, maps) then
735 http.header("X-CBI-State", state or 0)
737 if not config.noheader then
738 tpl.render("cbi/header", {state = state})
743 local applymap = false
744 local pageaction = true
745 local parsechain = { }
747 for i, res in ipairs(maps) do
748 if res.apply_needed and res.parsechain then
750 for _, c in ipairs(res.parsechain) do
751 parsechain[#parsechain+1] = c
757 redirect = redirect or res.redirect
760 if res.pageaction == false then
765 messages = messages or { }
766 messages[#messages+1] = res.message
770 for i, res in ipairs(maps) do
776 pageaction = pageaction,
777 parsechain = parsechain
781 if not config.nofooter then
782 tpl.render("cbi/footer", {
784 pageaction = pageaction,
787 autoapply = config.autoapply
792 --- Create a CBI model dispatching target.
793 -- @param model CBI model to be rendered
794 function cbi(model, config)
795 return {type = "cbi", config = config, model = model, target = _cbi}
799 local function _arcombine(self, ...)
801 local target = #argv > 0 and self.targets[2] or self.targets[1]
802 setfenv(target.target, self.env)
803 target:target(unpack(argv))
806 --- Create a combined dispatching target for non argv and argv requests.
807 -- @param trg1 Overview Target
808 -- @param trg2 Detail Target
809 function arcombine(trg1, trg2)
810 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
814 local function _form(self, ...)
815 local cbi = require "luci.cbi"
816 local tpl = require "luci.template"
817 local http = require "luci.http"
819 local maps = luci.cbi.load(self.model, ...)
822 for i, res in ipairs(maps) do
823 local cstate = res:parse()
824 if cstate and (not state or cstate < state) then
829 http.header("X-CBI-State", state or 0)
831 for i, res in ipairs(maps) do
837 --- Create a CBI form model dispatching target.
838 -- @param model CBI form model tpo be rendered
840 return {type = "cbi", model = model, target = _form}