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 url = { http.getenv("SCRIPT_NAME") or "" }
55 for k, v in pairs(context.urltoken) do
57 url[#url+1] = http.urlencode(k)
59 url[#url+1] = http.urlencode(v)
63 for _, p in ipairs(path) do
64 if p:match("^[a-zA-Z0-9_%-%./,;]+$") then
70 return table.concat(url, "")
73 --- Send a 404 error code and render the "error404" template if available.
74 -- @param message Custom error message (optional)
76 function error404(message)
77 luci.http.status(404, "Not Found")
78 message = message or "Not Found"
80 require("luci.template")
81 if not luci.util.copcall(luci.template.render, "error404") then
82 luci.http.prepare_content("text/plain")
83 luci.http.write(message)
88 --- Send a 500 error code and render the "error500" template if available.
89 -- @param message Custom error message (optional)#
91 function error500(message)
92 luci.util.perror(message)
93 if not context.template_header_sent then
94 luci.http.status(500, "Internal Server Error")
95 luci.http.prepare_content("text/plain")
96 luci.http.write(message)
98 require("luci.template")
99 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
100 luci.http.prepare_content("text/plain")
101 luci.http.write(message)
107 function authenticator.htmlauth(validator, accs, default)
108 local user = luci.http.formvalue("username")
109 local pass = luci.http.formvalue("password")
111 if user and validator(user, pass) then
116 require("luci.template")
118 luci.template.render("sysauth", {duser=default, fuser=user})
123 --- Dispatch an HTTP request.
124 -- @param request LuCI HTTP Request object
125 function httpdispatch(request, prefix)
126 luci.http.context.request = request
130 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
133 for _, node in ipairs(prefix) do
138 for node in pathinfo:gmatch("[^/]+") do
142 local stat, err = util.coxpcall(function()
143 dispatch(context.request)
148 --context._disable_memtrace()
151 --- Dispatches a LuCI virtual path.
152 -- @param request Virtual path
153 function dispatch(request)
154 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
157 ctx.urltoken = ctx.urltoken or {}
159 local conf = require "luci.config"
161 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
163 local lang = conf.main.lang or "auto"
164 if lang == "auto" then
165 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
166 for lpat in aclang:gmatch("[%w-]+") do
167 lpat = lpat and lpat:gsub("-", "_")
168 if conf.languages[lpat] then
174 require "luci.i18n".setlanguage(lang)
185 ctx.requestargs = ctx.requestargs or args
188 local token = ctx.urltoken
192 for i, s in ipairs(request) do
195 tkey, tval = s:match(";(%w+)=([a-fA-F0-9]*)")
210 util.update(track, c)
219 for j=n+1, #request do
220 args[#args+1] = request[j]
221 freq[#freq+1] = request[j]
225 ctx.requestpath = freq
229 require("luci.i18n").loadc(track.i18n)
232 -- Init template engine
233 if (c and c.index) or not track.notemplate then
234 local tpl = require("luci.template")
235 local media = track.mediaurlbase or luci.config.main.mediaurlbase
236 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
238 for name, theme in pairs(luci.config.themes) do
239 if name:sub(1,1) ~= "." and pcall(tpl.Template,
240 "themes/%s/header" % fs.basename(theme)) then
244 assert(media, "No valid theme found")
247 tpl.context.viewns = setmetatable({
248 write = luci.http.write;
249 include = function(name) tpl.Template(name):render(getfenv(2)) end;
250 translate = function(...) return require("luci.i18n").translate(...) end;
251 striptags = util.striptags;
253 theme = fs.basename(media);
254 resource = luci.config.main.resourcebase
255 }, {__index=function(table, key)
256 if key == "controller" then
258 elseif key == "REQUEST_URI" then
259 return build_url(unpack(ctx.requestpath))
261 return rawget(table, key) or _G[key]
266 track.dependent = (track.dependent ~= false)
267 assert(not track.dependent or not track.auto, "Access Violation")
269 if track.sysauth then
270 local sauth = require "luci.sauth"
272 local authen = type(track.sysauth_authenticator) == "function"
273 and track.sysauth_authenticator
274 or authenticator[track.sysauth_authenticator]
276 local def = (type(track.sysauth) == "string") and track.sysauth
277 local accs = def and {track.sysauth} or track.sysauth
278 local sess = ctx.authsession
279 local verifytoken = false
281 sess = luci.http.getcookie("sysauth")
282 sess = sess and sess:match("^[a-f0-9]*$")
286 local sdat = sauth.read(sess)
290 sdat = loadstring(sdat)
293 if not verifytoken or ctx.urltoken.stok == sdat.token then
297 local eu = http.getenv("HTTP_AUTH_USER")
298 local ep = http.getenv("HTTP_AUTH_PASS")
299 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
300 authen = function() return eu end
304 if not util.contains(accs, user) then
306 ctx.urltoken.stok = nil
307 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
308 if not user or not util.contains(accs, user) then
311 local sid = sess or luci.sys.uniqueid(16)
313 local token = luci.sys.uniqueid(16)
314 sauth.write(sid, util.get_bytecode({
317 secret=luci.sys.uniqueid(16)
319 ctx.urltoken.stok = token
321 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
322 ctx.authsession = sid
325 luci.http.status(403, "Forbidden")
329 ctx.authsession = sess
333 if track.setgroup then
334 luci.sys.process.setgroup(track.setgroup)
337 if track.setuser then
338 luci.sys.process.setuser(track.setuser)
343 if type(c.target) == "function" then
345 elseif type(c.target) == "table" then
346 target = c.target.target
350 if c and (c.index or type(target) == "function") then
352 ctx.requested = ctx.requested or ctx.dispatched
355 if c and c.index then
356 local tpl = require "luci.template"
358 if util.copcall(tpl.render, "indexer", {}) then
363 if type(target) == "function" then
364 util.copcall(function()
365 local oldenv = getfenv(target)
366 local module = require(c.module)
367 local env = setmetatable({}, {__index=
370 return rawget(tbl, key) or module[key] or oldenv[key]
376 if type(c.target) == "table" then
377 target(c.target, unpack(args))
386 --- Generate the dispatching index using the best possible strategy.
387 function createindex()
388 local path = luci.util.libpath() .. "/controller/"
389 local suff = { ".lua", ".lua.gz" }
391 if luci.util.copcall(require, "luci.fastindex") then
392 createindex_fastindex(path, suff)
394 createindex_plain(path, suff)
398 --- Generate the dispatching index using the fastindex C-indexer.
399 -- @param path Controller base directory
400 -- @param suffixes Controller file suffixes
401 function createindex_fastindex(path, suffixes)
405 fi = luci.fastindex.new("index")
406 for _, suffix in ipairs(suffixes) do
407 fi.add(path .. "*" .. suffix)
408 fi.add(path .. "*/*" .. suffix)
413 for k, v in pairs(fi.indexes) do
418 --- Generate the dispatching index using the native file-cache based strategy.
419 -- @param path Controller base directory
420 -- @param suffixes Controller file suffixes
421 function createindex_plain(path, suffixes)
422 local controllers = { }
423 for _, suffix in ipairs(suffixes) do
424 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
425 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
429 local cachedate = fs.stat(indexcache, "mtime")
432 for _, obj in ipairs(controllers) do
433 local omtime = fs.stat(path .. "/" .. obj, "mtime")
434 realdate = (omtime and omtime > realdate) and omtime or realdate
437 if cachedate > realdate then
439 sys.process.info("uid") == fs.stat(indexcache, "uid")
440 and fs.stat(indexcache, "modestr") == "rw-------",
441 "Fatal: Indexcache is not sane!"
444 index = loadfile(indexcache)()
452 for i,c in ipairs(controllers) do
453 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
454 for _, suffix in ipairs(suffixes) do
455 module = module:gsub(suffix.."$", "")
458 local mod = require(module)
459 local idx = mod.index
461 if type(idx) == "function" then
467 local f = nixio.open(indexcache, "w", 600)
468 f:writeall(util.get_bytecode(index))
473 --- Create the dispatching tree from the index.
474 -- Build the index before if it does not exist yet.
475 function createtree()
481 local tree = {nodes={}}
484 ctx.treecache = setmetatable({}, {__mode="v"})
488 -- Load default translation
489 require "luci.i18n".loadc("default")
491 local scope = setmetatable({}, {__index = luci.dispatcher})
493 for k, v in pairs(index) do
499 local function modisort(a,b)
500 return modi[a].order < modi[b].order
503 for _, v in util.spairs(modi, modisort) do
504 scope._NAME = v.module
505 setfenv(v.func, scope)
512 --- Register a tree modifier.
513 -- @param func Modifier function
514 -- @param order Modifier order value (optional)
515 function modifier(func, order)
516 context.modifiers[#context.modifiers+1] = {
524 --- Clone a node of the dispatching tree to another position.
525 -- @param path Virtual path destination
526 -- @param clone Virtual path source
527 -- @param title Destination node title (optional)
528 -- @param order Destination node order value (optional)
529 -- @return Dispatching tree node
530 function assign(path, clone, title, order)
531 local obj = node(unpack(path))
538 setmetatable(obj, {__index = _create_node(clone)})
543 --- Create a new dispatching node and define common parameters.
544 -- @param path Virtual path
545 -- @param target Target function to call when dispatched.
546 -- @param title Destination node title
547 -- @param order Destination node order value (optional)
548 -- @return Dispatching tree node
549 function entry(path, target, title, order)
550 local c = node(unpack(path))
555 c.module = getfenv(2)._NAME
560 --- Fetch or create a dispatching node without setting the target module or
561 -- enabling the node.
562 -- @param ... Virtual path
563 -- @return Dispatching tree node
565 return _create_node({...})
568 --- Fetch or create a new dispatching node.
569 -- @param ... Virtual path
570 -- @return Dispatching tree node
572 local c = _create_node({...})
574 c.module = getfenv(2)._NAME
580 function _create_node(path, cache)
585 cache = cache or context.treecache
586 local name = table.concat(path, ".")
587 local c = cache[name]
590 local new = {nodes={}, auto=true, path=util.clone(path)}
591 local last = table.remove(path)
593 c = _create_node(path, cache)
606 --- Create a redirect to another dispatching node.
607 -- @param ... Virtual path destination
611 for _, r in ipairs({...}) do
619 --- Rewrite the first x path values of the request.
620 -- @param n Number of path values to replace
621 -- @param ... Virtual path to replace removed path values with
622 function rewrite(n, ...)
625 local dispatched = util.clone(context.dispatched)
628 table.remove(dispatched, 1)
631 for i, r in ipairs(req) do
632 table.insert(dispatched, i, r)
635 for _, r in ipairs({...}) do
636 dispatched[#dispatched+1] = r
644 local function _call(self, ...)
645 if #self.argv > 0 then
646 return getfenv()[self.name](unpack(self.argv), ...)
648 return getfenv()[self.name](...)
652 --- Create a function-call dispatching target.
653 -- @param name Target function of local controller
654 -- @param ... Additional parameters passed to the function
655 function call(name, ...)
656 return {type = "call", argv = {...}, name = name, target = _call}
660 local _template = function(self, ...)
661 require "luci.template".render(self.view)
664 --- Create a template render dispatching target.
665 -- @param name Template to be rendered
666 function template(name)
667 return {type = "template", view = name, target = _template}
671 local function _cbi(self, ...)
672 local cbi = require "luci.cbi"
673 local tpl = require "luci.template"
674 local http = require "luci.http"
676 local config = self.config or {}
677 local maps = cbi.load(self.model, ...)
681 for i, res in ipairs(maps) do
683 local cstate = res:parse()
684 if cstate and (not state or cstate < state) then
689 local function _resolve_path(path)
690 return type(path) == "table" and build_url(unpack(path)) or path
693 if config.on_valid_to and state and state > 0 and state < 2 then
694 http.redirect(_resolve_path(config.on_valid_to))
698 if config.on_changed_to and state and state > 1 then
699 http.redirect(_resolve_path(config.on_changed_to))
703 if config.on_success_to and state and state > 0 then
704 http.redirect(_resolve_path(config.on_success_to))
708 if config.state_handler then
709 if not config.state_handler(state, maps) then
714 local pageaction = true
715 http.header("X-CBI-State", state or 0)
716 if not config.noheader then
717 tpl.render("cbi/header", {state = state})
719 for i, res in ipairs(maps) do
721 if res.pageaction == false then
725 if not config.nofooter then
726 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
730 --- Create a CBI model dispatching target.
731 -- @param model CBI model to be rendered
732 function cbi(model, config)
733 return {type = "cbi", config = config, model = model, target = _cbi}
737 local function _arcombine(self, ...)
739 local target = #argv > 0 and self.targets[2] or self.targets[1]
740 setfenv(target.target, self.env)
741 target:target(unpack(argv))
744 --- Create a combined dispatching target for non argv and argv requests.
745 -- @param trg1 Overview Target
746 -- @param trg2 Detail Target
747 function arcombine(trg1, trg2)
748 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
752 local function _form(self, ...)
753 local cbi = require "luci.cbi"
754 local tpl = require "luci.template"
755 local http = require "luci.http"
757 local maps = luci.cbi.load(self.model, ...)
760 for i, res in ipairs(maps) do
761 local cstate = res:parse()
762 if cstate and (not state or cstate < state) then
767 http.header("X-CBI-State", state or 0)
769 for i, res in ipairs(maps) do
775 --- Create a CBI form model dispatching target.
776 -- @param model CBI form model tpo be rendered
778 return {type = "cbi", model = model, target = _form}