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(...)
50 return context.scriptname .. "/" .. table.concat(arg, "/")
53 --- Send a 404 error code and render the "error404" template if available.
54 -- @param message Custom error message (optional)
56 function error404(message)
57 luci.http.status(404, "Not Found")
58 message = message or "Not Found"
60 require("luci.template")
61 if not luci.util.copcall(luci.template.render, "error404") then
62 luci.http.prepare_content("text/plain")
63 luci.http.write(message)
68 --- Send a 500 error code and render the "error500" template if available.
69 -- @param message Custom error message (optional)#
71 function error500(message)
72 luci.http.status(500, "Internal Server Error")
74 require("luci.template")
75 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
76 luci.http.prepare_content("text/plain")
77 luci.http.write(message)
82 function authenticator.htmlauth(validator, accs, default)
83 local user = luci.http.formvalue("username")
84 local pass = luci.http.formvalue("password")
86 if user and validator(user, pass) then
91 require("luci.template")
93 luci.template.render("sysauth", {duser=default, fuser=user})
98 --- Dispatch an HTTP request.
99 -- @param request LuCI HTTP Request object
100 function httpdispatch(request)
101 luci.http.context.request = request
103 local pathinfo = request:getenv("PATH_INFO") or ""
105 for node in pathinfo:gmatch("[^/]+") do
106 table.insert(context.request, node)
109 local stat, err = util.copcall(dispatch, context.request)
111 luci.util.perror(err)
117 --context._disable_memtrace()
120 --- Dispatches a LuCI virtual path.
121 -- @param request Virtual path
122 function dispatch(request)
123 --context._disable_memtrace = require "luci.debug".trap_memtrace()
126 ctx.scriptname = luci.http.getenv("SCRIPT_NAME") or ""
127 ctx.urltoken = ctx.urltoken or {}
129 require "luci.i18n".setlanguage(require "luci.config".main.lang)
140 ctx.requestargs = ctx.requestargs or args
143 local token = ctx.urltoken
146 for i, s in ipairs(request) do
149 tkey, tval = s:match(";(%w+)=(.*)")
163 util.update(track, c)
172 for j=n+1, #request do
173 table.insert(args, request[j])
177 for k, v in pairs(token) do
178 ctx.scriptname = ctx.scriptname .. "/;" .. k .. "=" ..
185 require("luci.i18n").loadc(track.i18n)
188 -- Init template engine
189 if (c and c.index) or not track.notemplate then
190 local tpl = require("luci.template")
191 local media = track.mediaurlbase or luci.config.main.mediaurlbase
192 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
194 for name, theme in pairs(luci.config.themes) do
195 if name:sub(1,1) ~= "." and pcall(tpl.Template,
196 "themes/%s/header" % fs.basename(theme)) then
200 assert(media, "No valid theme found")
203 local viewns = setmetatable({}, {__index=function(table, key)
204 if key == "controller" then
205 return ctx.scriptname
206 elseif key == "REQUEST_URI" then
207 return ctx.scriptname .. "/" .. table.concat(ctx.requested, "/")
209 return rawget(table, key) or _G[key]
212 tpl.context.viewns = viewns
213 viewns.write = luci.http.write
214 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
215 viewns.translate = function(...) return require("luci.i18n").translate(...) end
216 viewns.striptags = util.striptags
218 viewns.theme = fs.basename(media)
219 viewns.resource = luci.config.main.resourcebase
222 track.dependent = (track.dependent ~= false)
223 assert(not track.dependent or not track.auto, "Access Violation")
225 if track.sysauth then
226 local sauth = require "luci.sauth"
228 local authen = type(track.sysauth_authenticator) == "function"
229 and track.sysauth_authenticator
230 or authenticator[track.sysauth_authenticator]
232 local def = (type(track.sysauth) == "string") and track.sysauth
233 local accs = def and {track.sysauth} or track.sysauth
234 local sess = ctx.authsession
235 local verifytoken = true
237 sess = luci.http.getcookie("sysauth")
238 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 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
254 if not user or not util.contains(accs, user) then
257 local sid = sess or luci.sys.uniqueid(16)
258 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
260 local token = luci.sys.uniqueid(16)
261 sauth.write(sid, util.get_bytecode({
264 secret=luci.sys.uniqueid(16)
266 ctx.scriptname = ctx.scriptname .. "/;stok="..token
268 ctx.authsession = sid
271 luci.http.status(403, "Forbidden")
275 ctx.authsession = sess
279 if track.setgroup then
280 luci.sys.process.setgroup(track.setgroup)
283 if track.setuser then
284 luci.sys.process.setuser(track.setuser)
287 if c and (c.index or type(c.target) == "function") then
289 ctx.requested = ctx.requested or ctx.dispatched
292 if c and c.index then
293 local tpl = require "luci.template"
295 if util.copcall(tpl.render, "indexer", {}) then
300 if c and type(c.target) == "function" then
301 util.copcall(function()
302 local oldenv = getfenv(c.target)
303 local module = require(c.module)
304 local env = setmetatable({}, {__index=
307 return rawget(tbl, key) or module[key] or oldenv[key]
310 setfenv(c.target, env)
313 c.target(unpack(args))
319 --- Generate the dispatching index using the best possible strategy.
320 function createindex()
321 local path = luci.util.libpath() .. "/controller/"
324 if luci.util.copcall(require, "luci.fastindex") then
325 createindex_fastindex(path, suff)
327 createindex_plain(path, suff)
331 --- Generate the dispatching index using the fastindex C-indexer.
332 -- @param path Controller base directory
333 -- @param suffix Controller file suffix
334 function createindex_fastindex(path, suffix)
338 fi = luci.fastindex.new("index")
339 fi.add(path .. "*" .. suffix)
340 fi.add(path .. "*/*" .. suffix)
344 for k, v in pairs(fi.indexes) do
349 --- Generate the dispatching index using the native file-cache based strategy.
350 -- @param path Controller base directory
351 -- @param suffix Controller file suffix
352 function createindex_plain(path, suffix)
353 local controllers = util.combine(
354 luci.fs.glob(path .. "*" .. suffix) or {},
355 luci.fs.glob(path .. "*/*" .. suffix) or {}
359 local cachedate = fs.mtime(indexcache)
362 for _, obj in ipairs(controllers) do
363 local omtime = fs.mtime(path .. "/" .. obj)
364 realdate = (omtime and omtime > realdate) and omtime or realdate
367 if cachedate > realdate then
369 sys.process.info("uid") == fs.stat(indexcache, "uid")
370 and fs.stat(indexcache, "mode") == "rw-------",
371 "Fatal: Indexcache is not sane!"
374 index = loadfile(indexcache)()
382 for i,c in ipairs(controllers) do
383 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
384 local mod = require(module)
385 local idx = mod.index
387 if type(idx) == "function" then
393 fs.writefile(indexcache, util.get_bytecode(index))
394 fs.chmod(indexcache, "a-rwx,u+rw")
398 --- Create the dispatching tree from the index.
399 -- Build the index before if it does not exist yet.
400 function createtree()
406 local tree = {nodes={}}
409 ctx.treecache = setmetatable({}, {__mode="v"})
413 -- Load default translation
414 require "luci.i18n".loadc("default")
416 local scope = setmetatable({}, {__index = luci.dispatcher})
418 for k, v in pairs(index) do
424 local function modisort(a,b)
425 return modi[a].order < modi[b].order
428 for _, v in util.spairs(modi, modisort) do
429 scope._NAME = v.module
430 setfenv(v.func, scope)
437 --- Register a tree modifier.
438 -- @param func Modifier function
439 -- @param order Modifier order value (optional)
440 function modifier(func, order)
441 context.modifiers[#context.modifiers+1] = {
449 --- Clone a node of the dispatching tree to another position.
450 -- @param path Virtual path destination
451 -- @param clone Virtual path source
452 -- @param title Destination node title (optional)
453 -- @param order Destination node order value (optional)
454 -- @return Dispatching tree node
455 function assign(path, clone, title, order)
456 local obj = node(unpack(path))
463 setmetatable(obj, {__index = _create_node(clone)})
468 --- Create a new dispatching node and define common parameters.
469 -- @param path Virtual path
470 -- @param target Target function to call when dispatched.
471 -- @param title Destination node title
472 -- @param order Destination node order value (optional)
473 -- @return Dispatching tree node
474 function entry(path, target, title, order)
475 local c = node(unpack(path))
480 c.module = getfenv(2)._NAME
485 --- Fetch or create a new dispatching node.
486 -- @param ... Virtual path
487 -- @return Dispatching tree node
489 local c = _create_node({...})
491 c.module = getfenv(2)._NAME
497 function _create_node(path, cache)
502 cache = cache or context.treecache
503 local name = table.concat(path, ".")
504 local c = cache[name]
507 local new = {nodes={}, auto=true, path=util.clone(path)}
508 local last = table.remove(path)
510 c = _create_node(path, cache)
523 --- Create a redirect to another dispatching node.
524 -- @param ... Virtual path destination
528 for _, r in ipairs({...}) do
536 --- Rewrite the first x path values of the request.
537 -- @param n Number of path values to replace
538 -- @param ... Virtual path to replace removed path values with
539 function rewrite(n, ...)
542 local dispatched = util.clone(context.dispatched)
545 table.remove(dispatched, 1)
548 for i, r in ipairs(req) do
549 table.insert(dispatched, i, r)
552 for _, r in ipairs({...}) do
553 dispatched[#dispatched+1] = r
560 --- Create a function-call dispatching target.
561 -- @param name Target function of local controller
562 -- @param ... Additional parameters passed to the function
563 function call(name, ...)
567 return getfenv()[name](unpack(argv), ...)
569 return getfenv()[name](...)
574 --- Create a template render dispatching target.
575 -- @param name Template to be rendered
576 function template(name)
578 require("luci.template")
579 luci.template.render(name)
583 --- Create a CBI model dispatching target.
584 -- @param model CBI model to be rendered
585 function cbi(model, config)
586 config = config or {}
589 require("luci.template")
590 local http = require "luci.http"
592 maps = luci.cbi.load(model, ...)
596 for i, res in ipairs(maps) do
597 if config.autoapply then
598 res.autoapply = config.autoapply
600 local cstate = res:parse()
601 if not state or cstate < state then
606 if config.on_valid_to and state and state > 0 and state < 2 then
607 luci.http.redirect(config.on_valid_to)
611 if config.on_changed_to and state and state > 1 then
612 luci.http.redirect(config.on_changed_to)
616 if config.on_success_to and state and state > 0 then
617 luci.http.redirect(config.on_success_to)
621 if config.state_handler then
622 if not config.state_handler(state, maps) then
627 local pageaction = true
628 http.header("X-CBI-State", state or 0)
629 luci.template.render("cbi/header", {state = state})
630 for i, res in ipairs(maps) do
632 if res.pageaction == false then
636 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
640 --- Create a CBI form model dispatching target.
641 -- @param model CBI form model tpo be rendered
645 require("luci.template")
646 local http = require "luci.http"
648 maps = luci.cbi.load(model, ...)
652 for i, res in ipairs(maps) do
653 local cstate = res:parse()
654 if not state or cstate < state then
659 http.header("X-CBI-State", state or 0)
660 luci.template.render("header")
661 for i, res in ipairs(maps) do
664 luci.template.render("footer")