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 "luci.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"
34 module("luci.dispatcher", package.seeall)
35 context = util.threadlocal()
46 --- Build the URL relative to the server webroot from given virtual path.
47 -- @param ... Virtual path
48 -- @return Relative URL
49 function build_url(...)
51 local sn = http.getenv("SCRIPT_NAME") or ""
52 for k, v in pairs(context.urltoken) do
53 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
55 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
58 --- Send a 404 error code and render the "error404" template if available.
59 -- @param message Custom error message (optional)
61 function error404(message)
62 luci.http.status(404, "Not Found")
63 message = message or "Not Found"
65 require("luci.template")
66 if not luci.util.copcall(luci.template.render, "error404") then
67 luci.http.prepare_content("text/plain")
68 luci.http.write(message)
73 --- Send a 500 error code and render the "error500" template if available.
74 -- @param message Custom error message (optional)#
76 function error500(message)
77 luci.util.perror(message)
78 if not context.template_header_sent then
79 luci.http.status(500, "Internal Server Error")
80 luci.http.prepare_content("text/plain")
81 luci.http.write(message)
83 require("luci.template")
84 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
85 luci.http.prepare_content("text/plain")
86 luci.http.write(message)
92 function authenticator.htmlauth(validator, accs, default)
93 local user = luci.http.formvalue("username")
94 local pass = luci.http.formvalue("password")
96 if user and validator(user, pass) then
101 require("luci.template")
103 luci.template.render("sysauth", {duser=default, fuser=user})
108 --- Dispatch an HTTP request.
109 -- @param request LuCI HTTP Request object
110 function httpdispatch(request)
111 luci.http.context.request = request
113 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
115 for node in pathinfo:gmatch("[^/]+") do
116 table.insert(context.request, node)
119 local stat, err = util.coxpcall(function()
120 dispatch(context.request)
125 --context._disable_memtrace()
128 --- Dispatches a LuCI virtual path.
129 -- @param request Virtual path
130 function dispatch(request)
131 --context._disable_memtrace = require "luci.debug".trap_memtrace()
134 ctx.urltoken = ctx.urltoken or {}
136 local conf = require "luci.config"
138 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
140 local lang = conf.main.lang
141 if lang == "auto" then
142 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
143 for lpat in aclang:gmatch("[%w-]+") do
144 lpat = lpat and lpat:gsub("-", "_")
145 if conf.languages[lpat] then
151 require "luci.i18n".setlanguage(lang)
162 ctx.requestargs = ctx.requestargs or args
165 local token = ctx.urltoken
169 for i, s in ipairs(request) do
172 tkey, tval = s:match(";(%w+)=(.*)")
187 util.update(track, c)
196 for j=n+1, #request do
197 args[#args+1] = request[j]
198 freq[#freq+1] = request[j]
202 ctx.requestpath = freq
206 require("luci.i18n").loadc(track.i18n)
209 -- Init template engine
210 if (c and c.index) or not track.notemplate then
211 local tpl = require("luci.template")
212 local media = track.mediaurlbase or luci.config.main.mediaurlbase
213 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
215 for name, theme in pairs(luci.config.themes) do
216 if name:sub(1,1) ~= "." and pcall(tpl.Template,
217 "themes/%s/header" % fs.basename(theme)) then
221 assert(media, "No valid theme found")
224 local viewns = setmetatable({}, {__index=function(table, key)
225 if key == "controller" then
227 elseif key == "REQUEST_URI" then
228 return build_url(unpack(ctx.requestpath))
230 return rawget(table, key) or _G[key]
233 tpl.context.viewns = viewns
234 viewns.write = luci.http.write
235 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
236 viewns.translate = function(...) return require("luci.i18n").translate(...) end
237 viewns.striptags = util.striptags
239 viewns.theme = fs.basename(media)
240 viewns.resource = luci.config.main.resourcebase
243 track.dependent = (track.dependent ~= false)
244 assert(not track.dependent or not track.auto, "Access Violation")
246 if track.sysauth then
247 local sauth = require "luci.sauth"
249 local authen = type(track.sysauth_authenticator) == "function"
250 and track.sysauth_authenticator
251 or authenticator[track.sysauth_authenticator]
253 local def = (type(track.sysauth) == "string") and track.sysauth
254 local accs = def and {track.sysauth} or track.sysauth
255 local sess = ctx.authsession
256 local verifytoken = false
258 sess = luci.http.getcookie("sysauth")
259 sess = sess and sess:match("^[A-F0-9]+$")
263 local sdat = sauth.read(sess)
267 sdat = loadstring(sdat)()
268 if not verifytoken or ctx.urltoken.stok == sdat.token then
273 if not util.contains(accs, user) then
275 ctx.urltoken.stok = nil
276 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
277 if not user or not util.contains(accs, user) then
280 local sid = sess or luci.sys.uniqueid(16)
282 local token = luci.sys.uniqueid(16)
283 sauth.write(sid, util.get_bytecode({
286 secret=luci.sys.uniqueid(16)
288 ctx.urltoken.stok = token
290 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
291 ctx.authsession = sid
294 luci.http.status(403, "Forbidden")
298 ctx.authsession = sess
302 if track.setgroup then
303 luci.sys.process.setgroup(track.setgroup)
306 if track.setuser then
307 luci.sys.process.setuser(track.setuser)
312 if type(c.target) == "function" then
314 elseif type(c.target) == "table" then
315 target = c.target.target
319 if c and (c.index or type(target) == "function") then
321 ctx.requested = ctx.requested or ctx.dispatched
324 if c and c.index then
325 local tpl = require "luci.template"
327 if util.copcall(tpl.render, "indexer", {}) then
332 if type(target) == "function" then
333 util.copcall(function()
334 local oldenv = getfenv(target)
335 local module = require(c.module)
336 local env = setmetatable({}, {__index=
339 return rawget(tbl, key) or module[key] or oldenv[key]
345 if type(c.target) == "table" then
346 target(c.target, unpack(args))
355 --- Generate the dispatching index using the best possible strategy.
356 function createindex()
357 local path = luci.util.libpath() .. "/controller/"
358 local suff = { ".lua", ".lua.gz" }
360 if luci.util.copcall(require, "luci.fastindex") then
361 createindex_fastindex(path, suff)
363 createindex_plain(path, suff)
367 --- Generate the dispatching index using the fastindex C-indexer.
368 -- @param path Controller base directory
369 -- @param suffixes Controller file suffixes
370 function createindex_fastindex(path, suffixes)
374 fi = luci.fastindex.new("index")
375 for _, suffix in ipairs(suffixes) do
376 fi.add(path .. "*" .. suffix)
377 fi.add(path .. "*/*" .. suffix)
382 for k, v in pairs(fi.indexes) do
387 --- Generate the dispatching index using the native file-cache based strategy.
388 -- @param path Controller base directory
389 -- @param suffixes Controller file suffixes
390 function createindex_plain(path, suffixes)
391 local controllers = { }
392 for _, suffix in ipairs(suffixes) do
393 controllers = util.combine(
395 luci.fs.glob(path .. "*" .. suffix) or {},
396 luci.fs.glob(path .. "*/*" .. suffix) or {}
401 local cachedate = fs.mtime(indexcache)
404 for _, obj in ipairs(controllers) do
405 local omtime = fs.mtime(path .. "/" .. obj)
406 realdate = (omtime and omtime > realdate) and omtime or realdate
409 if cachedate > realdate then
411 sys.process.info("uid") == fs.stat(indexcache, "uid")
412 and fs.stat(indexcache, "mode") == "rw-------",
413 "Fatal: Indexcache is not sane!"
416 index = loadfile(indexcache)()
424 for i,c in ipairs(controllers) do
425 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
426 for _, suffix in ipairs(suffixes) do
427 module = module:gsub(suffix.."$", "")
430 local mod = require(module)
431 local idx = mod.index
433 if type(idx) == "function" then
439 fs.writefile(indexcache, util.get_bytecode(index))
440 fs.chmod(indexcache, "a-rwx,u+rw")
444 --- Create the dispatching tree from the index.
445 -- Build the index before if it does not exist yet.
446 function createtree()
452 local tree = {nodes={}}
455 ctx.treecache = setmetatable({}, {__mode="v"})
459 -- Load default translation
460 require "luci.i18n".loadc("default")
462 local scope = setmetatable({}, {__index = luci.dispatcher})
464 for k, v in pairs(index) do
470 local function modisort(a,b)
471 return modi[a].order < modi[b].order
474 for _, v in util.spairs(modi, modisort) do
475 scope._NAME = v.module
476 setfenv(v.func, scope)
483 --- Register a tree modifier.
484 -- @param func Modifier function
485 -- @param order Modifier order value (optional)
486 function modifier(func, order)
487 context.modifiers[#context.modifiers+1] = {
495 --- Clone a node of the dispatching tree to another position.
496 -- @param path Virtual path destination
497 -- @param clone Virtual path source
498 -- @param title Destination node title (optional)
499 -- @param order Destination node order value (optional)
500 -- @return Dispatching tree node
501 function assign(path, clone, title, order)
502 local obj = node(unpack(path))
509 setmetatable(obj, {__index = _create_node(clone)})
514 --- Create a new dispatching node and define common parameters.
515 -- @param path Virtual path
516 -- @param target Target function to call when dispatched.
517 -- @param title Destination node title
518 -- @param order Destination node order value (optional)
519 -- @return Dispatching tree node
520 function entry(path, target, title, order)
521 local c = node(unpack(path))
526 c.module = getfenv(2)._NAME
531 --- Fetch or create a dispatching node without setting the target module or
532 -- enabling the node.
533 -- @param ... Virtual path
534 -- @return Dispatching tree node
536 return _create_node({...})
539 --- Fetch or create a new dispatching node.
540 -- @param ... Virtual path
541 -- @return Dispatching tree node
543 local c = _create_node({...})
545 c.module = getfenv(2)._NAME
551 function _create_node(path, cache)
556 cache = cache or context.treecache
557 local name = table.concat(path, ".")
558 local c = cache[name]
561 local new = {nodes={}, auto=true, path=util.clone(path)}
562 local last = table.remove(path)
564 c = _create_node(path, cache)
577 --- Create a redirect to another dispatching node.
578 -- @param ... Virtual path destination
582 for _, r in ipairs({...}) do
590 --- Rewrite the first x path values of the request.
591 -- @param n Number of path values to replace
592 -- @param ... Virtual path to replace removed path values with
593 function rewrite(n, ...)
596 local dispatched = util.clone(context.dispatched)
599 table.remove(dispatched, 1)
602 for i, r in ipairs(req) do
603 table.insert(dispatched, i, r)
606 for _, r in ipairs({...}) do
607 dispatched[#dispatched+1] = r
615 local function _call(self, ...)
616 if #self.argv > 0 then
617 return getfenv()[self.name](unpack(self.argv), ...)
619 return getfenv()[self.name](...)
623 --- Create a function-call dispatching target.
624 -- @param name Target function of local controller
625 -- @param ... Additional parameters passed to the function
626 function call(name, ...)
627 return {type = "call", argv = {...}, name = name, target = _call}
631 local _template = function(self, ...)
632 require "luci.template".render(self.view)
635 --- Create a template render dispatching target.
636 -- @param name Template to be rendered
637 function template(name)
638 return {type = "template", view = name, target = _template}
642 local function _cbi(self, ...)
643 local cbi = require "luci.cbi"
644 local tpl = require "luci.template"
645 local http = require "luci.http"
647 local config = self.config or {}
648 local maps = cbi.load(self.model, ...)
652 for i, res in ipairs(maps) do
654 local cstate = res:parse()
655 if cstate and (not state or cstate < state) then
660 if config.on_valid_to and state and state > 0 and state < 2 then
661 http.redirect(config.on_valid_to)
665 if config.on_changed_to and state and state > 1 then
666 http.redirect(config.on_changed_to)
670 if config.on_success_to and state and state > 0 then
671 http.redirect(config.on_success_to)
675 if config.state_handler then
676 if not config.state_handler(state, maps) then
681 local pageaction = true
682 http.header("X-CBI-State", state or 0)
683 if not config.noheader then
684 tpl.render("cbi/header", {state = state})
686 for i, res in ipairs(maps) do
688 if res.pageaction == false then
692 if not config.nofooter then
693 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
697 --- Create a CBI model dispatching target.
698 -- @param model CBI model to be rendered
699 function cbi(model, config)
700 return {type = "cbi", config = config, model = model, target = _cbi}
704 local function _arcombine(self, ...)
706 local target = #argv > 0 and self.targets[2] or self.targets[1]
707 setfenv(target.target, self.env)
708 target:target(unpack(argv))
711 --- Create a combined dispatching target for non argv and argv requests.
712 -- @param trg1 Overview Target
713 -- @param trg2 Detail Target
714 function arcombine(trg1, trg2)
715 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
719 local function _form(self, ...)
720 local cbi = require "luci.cbi"
721 local tpl = require "luci.template"
722 local http = require "luci.http"
724 local maps = luci.cbi.load(self.model, ...)
727 for i, res in ipairs(maps) do
728 local cstate = res:parse()
729 if cstate and (not state or cstate < state) then
734 http.header("X-CBI-State", state or 0)
736 for i, res in ipairs(maps) do
742 --- Create a CBI form model dispatching target.
743 -- @param model CBI form model tpo be rendered
745 return {type = "cbi", model = model, target = _form}