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.http.status(500, "Internal Server Error")
79 require("luci.template")
80 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
81 luci.http.prepare_content("text/plain")
82 luci.http.write(message)
87 function authenticator.htmlauth(validator, accs, default)
88 local user = luci.http.formvalue("username")
89 local pass = luci.http.formvalue("password")
91 if user and validator(user, pass) then
96 require("luci.template")
98 luci.template.render("sysauth", {duser=default, fuser=user})
103 --- Dispatch an HTTP request.
104 -- @param request LuCI HTTP Request object
105 function httpdispatch(request)
106 luci.http.context.request = request
108 local pathinfo = request:getenv("PATH_INFO") or ""
110 for node in pathinfo:gmatch("[^/]+") do
111 table.insert(context.request, node)
114 local stat, err = util.copcall(dispatch, context.request)
116 luci.util.perror(err)
122 --context._disable_memtrace()
125 --- Dispatches a LuCI virtual path.
126 -- @param request Virtual path
127 function dispatch(request)
128 --context._disable_memtrace = require "luci.debug".trap_memtrace()
131 ctx.urltoken = ctx.urltoken or {}
133 require "luci.i18n".setlanguage(require "luci.config".main.lang)
144 ctx.requestargs = ctx.requestargs or args
147 local token = ctx.urltoken
150 for i, s in ipairs(request) do
153 tkey, tval = s:match(";(%w+)=(.*)")
167 util.update(track, c)
176 for j=n+1, #request do
177 table.insert(args, request[j])
184 require("luci.i18n").loadc(track.i18n)
187 -- Init template engine
188 if (c and c.index) or not track.notemplate then
189 local tpl = require("luci.template")
190 local media = track.mediaurlbase or luci.config.main.mediaurlbase
191 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
193 for name, theme in pairs(luci.config.themes) do
194 if name:sub(1,1) ~= "." and pcall(tpl.Template,
195 "themes/%s/header" % fs.basename(theme)) then
199 assert(media, "No valid theme found")
202 local viewns = setmetatable({}, {__index=function(table, key)
203 if key == "controller" then
205 elseif key == "REQUEST_URI" then
206 return build_url(ctx.requested)
208 return rawget(table, key) or _G[key]
211 tpl.context.viewns = viewns
212 viewns.write = luci.http.write
213 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
214 viewns.translate = function(...) return require("luci.i18n").translate(...) end
215 viewns.striptags = util.striptags
217 viewns.theme = fs.basename(media)
218 viewns.resource = luci.config.main.resourcebase
221 track.dependent = (track.dependent ~= false)
222 assert(not track.dependent or not track.auto, "Access Violation")
224 if track.sysauth then
225 local sauth = require "luci.sauth"
227 local authen = type(track.sysauth_authenticator) == "function"
228 and track.sysauth_authenticator
229 or authenticator[track.sysauth_authenticator]
231 local def = (type(track.sysauth) == "string") and track.sysauth
232 local accs = def and {track.sysauth} or track.sysauth
233 local sess = ctx.authsession
234 local verifytoken = false
236 sess = luci.http.getcookie("sysauth")
237 sess = sess and sess:match("^[A-F0-9]+$")
241 local sdat = sauth.read(sess)
245 sdat = loadstring(sdat)()
246 if not verifytoken or ctx.urltoken.stok == sdat.token then
251 if not util.contains(accs, user) then
253 ctx.urltoken.stok = nil
254 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
255 if not user or not util.contains(accs, user) then
258 local sid = sess or luci.sys.uniqueid(16)
260 local token = luci.sys.uniqueid(16)
261 sauth.write(sid, util.get_bytecode({
264 secret=luci.sys.uniqueid(16)
266 ctx.urltoken.stok = token
268 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
269 ctx.authsession = sid
272 luci.http.status(403, "Forbidden")
276 ctx.authsession = sess
280 if track.setgroup then
281 luci.sys.process.setgroup(track.setgroup)
284 if track.setuser then
285 luci.sys.process.setuser(track.setuser)
288 if c and (c.index or type(c.target) == "function") then
290 ctx.requested = ctx.requested or ctx.dispatched
293 if c and c.index then
294 local tpl = require "luci.template"
296 if util.copcall(tpl.render, "indexer", {}) then
301 if c and type(c.target) == "function" then
302 util.copcall(function()
303 local oldenv = getfenv(c.target)
304 local module = require(c.module)
305 local env = setmetatable({}, {__index=
308 return rawget(tbl, key) or module[key] or oldenv[key]
311 setfenv(c.target, env)
314 c.target(unpack(args))
320 --- Generate the dispatching index using the best possible strategy.
321 function createindex()
322 local path = luci.util.libpath() .. "/controller/"
325 if luci.util.copcall(require, "luci.fastindex") then
326 createindex_fastindex(path, suff)
328 createindex_plain(path, suff)
332 --- Generate the dispatching index using the fastindex C-indexer.
333 -- @param path Controller base directory
334 -- @param suffix Controller file suffix
335 function createindex_fastindex(path, suffix)
339 fi = luci.fastindex.new("index")
340 fi.add(path .. "*" .. suffix)
341 fi.add(path .. "*/*" .. suffix)
345 for k, v in pairs(fi.indexes) do
350 --- Generate the dispatching index using the native file-cache based strategy.
351 -- @param path Controller base directory
352 -- @param suffix Controller file suffix
353 function createindex_plain(path, suffix)
354 local controllers = util.combine(
355 luci.fs.glob(path .. "*" .. suffix) or {},
356 luci.fs.glob(path .. "*/*" .. suffix) or {}
360 local cachedate = fs.mtime(indexcache)
363 for _, obj in ipairs(controllers) do
364 local omtime = fs.mtime(path .. "/" .. obj)
365 realdate = (omtime and omtime > realdate) and omtime or realdate
368 if cachedate > realdate then
370 sys.process.info("uid") == fs.stat(indexcache, "uid")
371 and fs.stat(indexcache, "mode") == "rw-------",
372 "Fatal: Indexcache is not sane!"
375 index = loadfile(indexcache)()
383 for i,c in ipairs(controllers) do
384 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
385 local mod = require(module)
386 local idx = mod.index
388 if type(idx) == "function" then
394 fs.writefile(indexcache, util.get_bytecode(index))
395 fs.chmod(indexcache, "a-rwx,u+rw")
399 --- Create the dispatching tree from the index.
400 -- Build the index before if it does not exist yet.
401 function createtree()
407 local tree = {nodes={}}
410 ctx.treecache = setmetatable({}, {__mode="v"})
414 -- Load default translation
415 require "luci.i18n".loadc("default")
417 local scope = setmetatable({}, {__index = luci.dispatcher})
419 for k, v in pairs(index) do
425 local function modisort(a,b)
426 return modi[a].order < modi[b].order
429 for _, v in util.spairs(modi, modisort) do
430 scope._NAME = v.module
431 setfenv(v.func, scope)
438 --- Register a tree modifier.
439 -- @param func Modifier function
440 -- @param order Modifier order value (optional)
441 function modifier(func, order)
442 context.modifiers[#context.modifiers+1] = {
450 --- Clone a node of the dispatching tree to another position.
451 -- @param path Virtual path destination
452 -- @param clone Virtual path source
453 -- @param title Destination node title (optional)
454 -- @param order Destination node order value (optional)
455 -- @return Dispatching tree node
456 function assign(path, clone, title, order)
457 local obj = node(unpack(path))
464 setmetatable(obj, {__index = _create_node(clone)})
469 --- Create a new dispatching node and define common parameters.
470 -- @param path Virtual path
471 -- @param target Target function to call when dispatched.
472 -- @param title Destination node title
473 -- @param order Destination node order value (optional)
474 -- @return Dispatching tree node
475 function entry(path, target, title, order)
476 local c = node(unpack(path))
481 c.module = getfenv(2)._NAME
486 --- Fetch or create a new dispatching node.
487 -- @param ... Virtual path
488 -- @return Dispatching tree node
490 local c = _create_node({...})
492 c.module = getfenv(2)._NAME
498 function _create_node(path, cache)
503 cache = cache or context.treecache
504 local name = table.concat(path, ".")
505 local c = cache[name]
508 local new = {nodes={}, auto=true, path=util.clone(path)}
509 local last = table.remove(path)
511 c = _create_node(path, cache)
524 --- Create a redirect to another dispatching node.
525 -- @param ... Virtual path destination
529 for _, r in ipairs({...}) do
537 --- Rewrite the first x path values of the request.
538 -- @param n Number of path values to replace
539 -- @param ... Virtual path to replace removed path values with
540 function rewrite(n, ...)
543 local dispatched = util.clone(context.dispatched)
546 table.remove(dispatched, 1)
549 for i, r in ipairs(req) do
550 table.insert(dispatched, i, r)
553 for _, r in ipairs({...}) do
554 dispatched[#dispatched+1] = r
561 --- Create a function-call dispatching target.
562 -- @param name Target function of local controller
563 -- @param ... Additional parameters passed to the function
564 function call(name, ...)
568 return getfenv()[name](unpack(argv), ...)
570 return getfenv()[name](...)
575 --- Create a template render dispatching target.
576 -- @param name Template to be rendered
577 function template(name)
579 require("luci.template")
580 luci.template.render(name)
584 --- Create a CBI model dispatching target.
585 -- @param model CBI model to be rendered
586 function cbi(model, config)
587 config = config or {}
590 require("luci.template")
591 local http = require "luci.http"
593 maps = luci.cbi.load(model, ...)
597 for i, res in ipairs(maps) do
598 if config.autoapply then
599 res.autoapply = config.autoapply
601 local cstate = res:parse()
602 if not state or cstate < state then
607 if config.on_valid_to and state and state > 0 and state < 2 then
608 luci.http.redirect(config.on_valid_to)
612 if config.on_changed_to and state and state > 1 then
613 luci.http.redirect(config.on_changed_to)
617 if config.on_success_to and state and state > 0 then
618 luci.http.redirect(config.on_success_to)
622 if config.state_handler then
623 if not config.state_handler(state, maps) then
628 local pageaction = true
629 http.header("X-CBI-State", state or 0)
630 luci.template.render("cbi/header", {state = state})
631 for i, res in ipairs(maps) do
633 if res.pageaction == false then
637 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
641 --- Create a CBI form model dispatching target.
642 -- @param model CBI form model tpo be rendered
646 require("luci.template")
647 local http = require "luci.http"
649 maps = luci.cbi.load(model, ...)
653 for i, res in ipairs(maps) do
654 local cstate = res:parse()
655 if not state or cstate < state then
660 http.header("X-CBI-State", state or 0)
661 luci.template.render("header")
662 for i, res in ipairs(maps) do
665 luci.template.render("footer")