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"
49 --- Build the URL relative to the server webroot from given virtual path.
50 -- @param ... Virtual path
51 -- @return Relative URL
52 function build_url(...)
54 local sn = http.getenv("SCRIPT_NAME") or ""
55 for k, v in pairs(context.urltoken) do
56 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
58 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
61 --- Send a 404 error code and render the "error404" template if available.
62 -- @param message Custom error message (optional)
64 function error404(message)
65 luci.http.status(404, "Not Found")
66 message = message or "Not Found"
68 require("luci.template")
69 if not luci.util.copcall(luci.template.render, "error404") then
70 luci.http.prepare_content("text/plain")
71 luci.http.write(message)
76 --- Send a 500 error code and render the "error500" template if available.
77 -- @param message Custom error message (optional)#
79 function error500(message)
80 luci.util.perror(message)
81 if not context.template_header_sent then
82 luci.http.status(500, "Internal Server Error")
83 luci.http.prepare_content("text/plain")
84 luci.http.write(message)
86 require("luci.template")
87 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
88 luci.http.prepare_content("text/plain")
89 luci.http.write(message)
95 function authenticator.htmlauth(validator, accs, default)
96 local user = luci.http.formvalue("username")
97 local pass = luci.http.formvalue("password")
99 if user and validator(user, pass) then
104 require("luci.template")
106 luci.template.render("sysauth", {duser=default, fuser=user})
111 --- Dispatch an HTTP request.
112 -- @param request LuCI HTTP Request object
113 function httpdispatch(request, prefix)
114 luci.http.context.request = request
118 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
121 for _, node in ipairs(prefix) do
126 for node in pathinfo:gmatch("[^/]+") do
130 local stat, err = util.coxpcall(function()
131 dispatch(context.request)
136 --context._disable_memtrace()
139 --- Dispatches a LuCI virtual path.
140 -- @param request Virtual path
141 function dispatch(request)
142 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
145 ctx.urltoken = ctx.urltoken or {}
147 local conf = require "luci.config"
149 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
151 local lang = conf.main.lang
152 if lang == "auto" then
153 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
154 for lpat in aclang:gmatch("[%w-]+") do
155 lpat = lpat and lpat:gsub("-", "_")
156 if conf.languages[lpat] then
162 require "luci.i18n".setlanguage(lang)
173 ctx.requestargs = ctx.requestargs or args
176 local token = ctx.urltoken
180 for i, s in ipairs(request) do
183 tkey, tval = s:match(";(%w+)=(.*)")
198 util.update(track, c)
207 for j=n+1, #request do
208 args[#args+1] = request[j]
209 freq[#freq+1] = request[j]
213 ctx.requestpath = ctx.requestpath or freq
217 require("luci.i18n").loadc(track.i18n)
220 -- Init template engine
221 if (c and c.index) or not track.notemplate then
222 local tpl = require("luci.template")
223 local media = track.mediaurlbase or luci.config.main.mediaurlbase
224 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
226 for name, theme in pairs(luci.config.themes) do
227 if name:sub(1,1) ~= "." and pcall(tpl.Template,
228 "themes/%s/header" % fs.basename(theme)) then
232 assert(media, "No valid theme found")
235 tpl.context.viewns = setmetatable({
236 write = luci.http.write;
237 include = function(name) tpl.Template(name):render(getfenv(2)) end;
238 translate = function(...) return require("luci.i18n").translate(...) end;
239 striptags = util.striptags;
240 pcdata = util.pcdata;
242 theme = fs.basename(media);
243 resource = luci.config.main.resourcebase
244 }, {__index=function(table, key)
245 if key == "controller" then
247 elseif key == "REQUEST_URI" then
248 return build_url(unpack(ctx.requestpath))
250 return rawget(table, key) or _G[key]
255 track.dependent = (track.dependent ~= false)
256 assert(not track.dependent or not track.auto, "Access Violation")
258 if track.sysauth then
259 local sauth = require "luci.sauth"
261 local authen = type(track.sysauth_authenticator) == "function"
262 and track.sysauth_authenticator
263 or authenticator[track.sysauth_authenticator]
265 local def = (type(track.sysauth) == "string") and track.sysauth
266 local accs = def and {track.sysauth} or track.sysauth
267 local sess = ctx.authsession
268 local verifytoken = false
270 sess = luci.http.getcookie("sysauth")
271 sess = sess and sess:match("^[a-f0-9]*$")
275 local sdat = sauth.read(sess)
279 sdat = loadstring(sdat)
282 if not verifytoken or ctx.urltoken.stok == sdat.token then
286 local eu = http.getenv("HTTP_AUTH_USER")
287 local ep = http.getenv("HTTP_AUTH_PASS")
288 if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
289 authen = function() return eu end
293 if not util.contains(accs, user) then
295 ctx.urltoken.stok = nil
296 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
297 if not user or not util.contains(accs, user) then
300 local sid = sess or luci.sys.uniqueid(16)
302 local token = luci.sys.uniqueid(16)
303 sauth.write(sid, util.get_bytecode({
306 secret=luci.sys.uniqueid(16)
308 ctx.urltoken.stok = token
310 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
311 ctx.authsession = sid
315 luci.http.status(403, "Forbidden")
319 ctx.authsession = sess
324 if track.setgroup then
325 luci.sys.process.setgroup(track.setgroup)
328 if track.setuser then
329 luci.sys.process.setuser(track.setuser)
334 if type(c.target) == "function" then
336 elseif type(c.target) == "table" then
337 target = c.target.target
341 if c and (c.index or type(target) == "function") then
343 ctx.requested = ctx.requested or ctx.dispatched
346 if c and c.index then
347 local tpl = require "luci.template"
349 if util.copcall(tpl.render, "indexer", {}) then
354 if type(target) == "function" then
355 util.copcall(function()
356 local oldenv = getfenv(target)
357 local module = require(c.module)
358 local env = setmetatable({}, {__index=
361 return rawget(tbl, key) or module[key] or oldenv[key]
367 if type(c.target) == "table" then
368 target(c.target, unpack(args))
377 --- Generate the dispatching index using the best possible strategy.
378 function createindex()
379 local path = luci.util.libpath() .. "/controller/"
380 local suff = { ".lua", ".lua.gz" }
382 if luci.util.copcall(require, "luci.fastindex") then
383 createindex_fastindex(path, suff)
385 createindex_plain(path, suff)
389 --- Generate the dispatching index using the fastindex C-indexer.
390 -- @param path Controller base directory
391 -- @param suffixes Controller file suffixes
392 function createindex_fastindex(path, suffixes)
396 fi = luci.fastindex.new("index")
397 for _, suffix in ipairs(suffixes) do
398 fi.add(path .. "*" .. suffix)
399 fi.add(path .. "*/*" .. suffix)
404 for k, v in pairs(fi.indexes) do
409 --- Generate the dispatching index using the native file-cache based strategy.
410 -- @param path Controller base directory
411 -- @param suffixes Controller file suffixes
412 function createindex_plain(path, suffixes)
413 local controllers = { }
414 for _, suffix in ipairs(suffixes) do
415 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
416 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
420 local cachedate = fs.stat(indexcache, "mtime")
423 for _, obj in ipairs(controllers) do
424 local omtime = fs.stat(path .. "/" .. obj, "mtime")
425 realdate = (omtime and omtime > realdate) and omtime or realdate
428 if cachedate > realdate then
430 sys.process.info("uid") == fs.stat(indexcache, "uid")
431 and fs.stat(indexcache, "modestr") == "rw-------",
432 "Fatal: Indexcache is not sane!"
435 index = loadfile(indexcache)()
443 for i,c in ipairs(controllers) do
444 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
445 for _, suffix in ipairs(suffixes) do
446 module = module:gsub(suffix.."$", "")
449 local mod = require(module)
450 local idx = mod.index
452 if type(idx) == "function" then
458 local f = nixio.open(indexcache, "w", 600)
459 f:writeall(util.get_bytecode(index))
464 --- Create the dispatching tree from the index.
465 -- Build the index before if it does not exist yet.
466 function createtree()
472 local tree = {nodes={}}
475 ctx.treecache = setmetatable({}, {__mode="v"})
479 -- Load default translation
480 require "luci.i18n".loadc("base")
482 local scope = setmetatable({}, {__index = luci.dispatcher})
484 for k, v in pairs(index) do
490 local function modisort(a,b)
491 return modi[a].order < modi[b].order
494 for _, v in util.spairs(modi, modisort) do
495 scope._NAME = v.module
496 setfenv(v.func, scope)
503 --- Register a tree modifier.
504 -- @param func Modifier function
505 -- @param order Modifier order value (optional)
506 function modifier(func, order)
507 context.modifiers[#context.modifiers+1] = {
515 --- Clone a node of the dispatching tree to another position.
516 -- @param path Virtual path destination
517 -- @param clone Virtual path source
518 -- @param title Destination node title (optional)
519 -- @param order Destination node order value (optional)
520 -- @return Dispatching tree node
521 function assign(path, clone, title, order)
522 local obj = node(unpack(path))
529 setmetatable(obj, {__index = _create_node(clone)})
534 --- Create a new dispatching node and define common parameters.
535 -- @param path Virtual path
536 -- @param target Target function to call when dispatched.
537 -- @param title Destination node title
538 -- @param order Destination node order value (optional)
539 -- @return Dispatching tree node
540 function entry(path, target, title, order)
541 local c = node(unpack(path))
546 c.module = getfenv(2)._NAME
551 --- Fetch or create a dispatching node without setting the target module or
552 -- enabling the node.
553 -- @param ... Virtual path
554 -- @return Dispatching tree node
556 return _create_node({...})
559 --- Fetch or create a new dispatching node.
560 -- @param ... Virtual path
561 -- @return Dispatching tree node
563 local c = _create_node({...})
565 c.module = getfenv(2)._NAME
571 function _create_node(path, cache)
576 cache = cache or context.treecache
577 local name = table.concat(path, ".")
578 local c = cache[name]
581 local new = {nodes={}, auto=true, path=util.clone(path)}
582 local last = table.remove(path)
584 c = _create_node(path, cache)
597 --- Create a redirect to another dispatching node.
598 -- @param ... Virtual path destination
602 for _, r in ipairs({...}) do
610 --- Rewrite the first x path values of the request.
611 -- @param n Number of path values to replace
612 -- @param ... Virtual path to replace removed path values with
613 function rewrite(n, ...)
616 local dispatched = util.clone(context.dispatched)
619 table.remove(dispatched, 1)
622 for i, r in ipairs(req) do
623 table.insert(dispatched, i, r)
626 for _, r in ipairs({...}) do
627 dispatched[#dispatched+1] = r
635 local function _call(self, ...)
636 if #self.argv > 0 then
637 return getfenv()[self.name](unpack(self.argv), ...)
639 return getfenv()[self.name](...)
643 --- Create a function-call dispatching target.
644 -- @param name Target function of local controller
645 -- @param ... Additional parameters passed to the function
646 function call(name, ...)
647 return {type = "call", argv = {...}, name = name, target = _call}
651 local _template = function(self, ...)
652 require "luci.template".render(self.view)
655 --- Create a template render dispatching target.
656 -- @param name Template to be rendered
657 function template(name)
658 return {type = "template", view = name, target = _template}
662 local function _cbi(self, ...)
663 local cbi = require "luci.cbi"
664 local tpl = require "luci.template"
665 local http = require "luci.http"
667 local config = self.config or {}
668 local maps = cbi.load(self.model, ...)
672 for i, res in ipairs(maps) do
674 local cstate = res:parse()
675 if cstate and (not state or cstate < state) then
680 local function _resolve_path(path)
681 return type(path) == "table" and build_url(unpack(path)) or path
684 if config.on_valid_to and state and state > 0 and state < 2 then
685 http.redirect(_resolve_path(config.on_valid_to))
689 if config.on_changed_to and state and state > 1 then
690 http.redirect(_resolve_path(config.on_changed_to))
694 if config.on_success_to and state and state > 0 then
695 http.redirect(_resolve_path(config.on_success_to))
699 if config.state_handler then
700 if not config.state_handler(state, maps) then
705 local pageaction = true
706 http.header("X-CBI-State", state or 0)
707 if not config.noheader then
708 tpl.render("cbi/header", {state = state})
710 for i, res in ipairs(maps) do
712 if res.pageaction == false then
716 if not config.nofooter then
717 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
721 --- Create a CBI model dispatching target.
722 -- @param model CBI model to be rendered
723 function cbi(model, config)
724 return {type = "cbi", config = config, model = model, target = _cbi}
728 local function _arcombine(self, ...)
730 local target = #argv > 0 and self.targets[2] or self.targets[1]
731 setfenv(target.target, self.env)
732 target:target(unpack(argv))
735 --- Create a combined dispatching target for non argv and argv requests.
736 -- @param trg1 Overview Target
737 -- @param trg2 Detail Target
738 function arcombine(trg1, trg2)
739 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
743 local function _form(self, ...)
744 local cbi = require "luci.cbi"
745 local tpl = require "luci.template"
746 local http = require "luci.http"
748 local maps = luci.cbi.load(self.model, ...)
751 for i, res in ipairs(maps) do
752 local cstate = res:parse()
753 if cstate and (not state or cstate < state) then
758 http.header("X-CBI-State", state or 0)
760 for i, res in ipairs(maps) do
766 --- Create a CBI form model dispatching target.
767 -- @param model CBI form model tpo be rendered
769 return {type = "cbi", model = model, target = _form}