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 = luci.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 luci.http.getenv("SCRIPT_NAME") .. "/" .. 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()
127 require "luci.i18n".setlanguage(require "luci.config".main.lang)
138 ctx.requestargs = ctx.requestargs or args
141 for i, s in ipairs(request) do
148 util.update(track, c)
156 for j=n+1, #request do
157 table.insert(args, request[j])
162 require("luci.i18n").loadc(track.i18n)
165 -- Init template engine
166 if (c and c.index) or not track.notemplate then
167 local tpl = require("luci.template")
168 local media = track.mediaurlbase or luci.config.main.mediaurlbase
169 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
171 for name, theme in pairs(luci.config.themes) do
172 if name:sub(1,1) ~= "." and pcall(tpl.Template,
173 "themes/%s/header" % fs.basename(theme)) then
177 assert(media, "No valid theme found")
180 local viewns = setmetatable({}, {__index=_G})
181 tpl.context.viewns = viewns
182 viewns.write = luci.http.write
183 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
184 viewns.translate = function(...) return require("luci.i18n").translate(...) end
185 viewns.striptags = util.striptags
186 viewns.controller = luci.http.getenv("SCRIPT_NAME")
188 viewns.theme = fs.basename(media)
189 viewns.resource = luci.config.main.resourcebase
190 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
193 track.dependent = (track.dependent ~= false)
194 assert(not track.dependent or not track.auto, "Access Violation")
196 if track.sysauth then
197 local sauth = require "luci.sauth"
199 local authen = type(track.sysauth_authenticator) == "function"
200 and track.sysauth_authenticator
201 or authenticator[track.sysauth_authenticator]
203 local def = (type(track.sysauth) == "string") and track.sysauth
204 local accs = def and {track.sysauth} or track.sysauth
205 local sess = ctx.authsession or luci.http.getcookie("sysauth")
206 sess = sess and sess:match("^[A-F0-9]+$")
207 local user = sauth.read(sess)
209 if not util.contains(accs, user) then
211 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
212 if not user or not util.contains(accs, user) then
215 local sid = sess or luci.sys.uniqueid(16)
216 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
218 sauth.write(sid, user)
220 ctx.authsession = sid
223 luci.http.status(403, "Forbidden")
229 if track.setgroup then
230 luci.sys.process.setgroup(track.setgroup)
233 if track.setuser then
234 luci.sys.process.setuser(track.setuser)
237 if c and (c.index or type(c.target) == "function") then
239 ctx.requested = ctx.requested or ctx.dispatched
242 if c and c.index then
243 local tpl = require "luci.template"
245 if util.copcall(tpl.render, "indexer", {}) then
250 if c and type(c.target) == "function" then
251 util.copcall(function()
252 local oldenv = getfenv(c.target)
253 local module = require(c.module)
254 local env = setmetatable({}, {__index=
257 return rawget(tbl, key) or module[key] or oldenv[key]
260 setfenv(c.target, env)
263 c.target(unpack(args))
269 --- Generate the dispatching index using the best possible strategy.
270 function createindex()
271 local path = luci.util.libpath() .. "/controller/"
274 if luci.util.copcall(require, "luci.fastindex") then
275 createindex_fastindex(path, suff)
277 createindex_plain(path, suff)
281 --- Generate the dispatching index using the fastindex C-indexer.
282 -- @param path Controller base directory
283 -- @param suffix Controller file suffix
284 function createindex_fastindex(path, suffix)
288 fi = luci.fastindex.new("index")
289 fi.add(path .. "*" .. suffix)
290 fi.add(path .. "*/*" .. suffix)
294 for k, v in pairs(fi.indexes) do
299 --- Generate the dispatching index using the native file-cache based strategy.
300 -- @param path Controller base directory
301 -- @param suffix Controller file suffix
302 function createindex_plain(path, suffix)
303 local controllers = util.combine(
304 luci.fs.glob(path .. "*" .. suffix) or {},
305 luci.fs.glob(path .. "*/*" .. suffix) or {}
309 local cachedate = fs.mtime(indexcache)
312 for _, obj in ipairs(controllers) do
313 local omtime = fs.mtime(path .. "/" .. obj)
314 realdate = (omtime and omtime > realdate) and omtime or realdate
317 if cachedate > realdate then
319 sys.process.info("uid") == fs.stat(indexcache, "uid")
320 and fs.stat(indexcache, "mode") == "rw-------",
321 "Fatal: Indexcache is not sane!"
324 index = loadfile(indexcache)()
332 for i,c in ipairs(controllers) do
333 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
334 local mod = require(module)
335 local idx = mod.index
337 if type(idx) == "function" then
343 fs.writefile(indexcache, util.get_bytecode(index))
344 fs.chmod(indexcache, "a-rwx,u+rw")
348 --- Create the dispatching tree from the index.
349 -- Build the index before if it does not exist yet.
350 function createtree()
356 local tree = {nodes={}}
359 ctx.treecache = setmetatable({}, {__mode="v"})
363 -- Load default translation
364 require "luci.i18n".loadc("default")
366 local scope = setmetatable({}, {__index = luci.dispatcher})
368 for k, v in pairs(index) do
374 local function modisort(a,b)
375 return modi[a].order < modi[b].order
378 for _, v in util.spairs(modi, modisort) do
379 scope._NAME = v.module
380 setfenv(v.func, scope)
387 --- Register a tree modifier.
388 -- @param func Modifier function
389 -- @param order Modifier order value (optional)
390 function modifier(func, order)
391 context.modifiers[#context.modifiers+1] = {
394 module = getfenv(2)._NAME
398 --- Clone a node of the dispatching tree to another position.
399 -- @param path Virtual path destination
400 -- @param clone Virtual path source
401 -- @param title Destination node title (optional)
402 -- @param order Destination node order value (optional)
403 -- @return Dispatching tree node
404 function assign(path, clone, title, order)
405 local obj = node(unpack(path))
412 setmetatable(obj, {__index = _create_node(clone)})
417 --- Create a new dispatching node and define common parameters.
418 -- @param path Virtual path
419 -- @param target Target function to call when dispatched.
420 -- @param title Destination node title
421 -- @param order Destination node order value (optional)
422 -- @return Dispatching tree node
423 function entry(path, target, title, order)
424 local c = node(unpack(path))
429 c.module = getfenv(2)._NAME
434 --- Fetch or create a new dispatching node.
435 -- @param ... Virtual path
436 -- @return Dispatching tree node
438 local c = _create_node({...})
440 c.module = getfenv(2)._NAME
446 function _create_node(path, cache)
451 cache = cache or context.treecache
452 local name = table.concat(path, ".")
453 local c = cache[name]
456 local new = {nodes={}, auto=true, path=util.clone(path)}
457 local last = table.remove(path)
459 c = _create_node(path, cache)
472 --- Create a redirect to another dispatching node.
473 -- @param ... Virtual path destination
477 for _, r in ipairs({...}) do
485 --- Rewrite the first x path values of the request.
486 -- @param n Number of path values to replace
487 -- @param ... Virtual path to replace removed path values with
488 function rewrite(n, ...)
491 local dispatched = util.clone(context.dispatched)
494 table.remove(dispatched, 1)
497 for i, r in ipairs(req) do
498 table.insert(dispatched, i, r)
501 for _, r in ipairs({...}) do
502 dispatched[#dispatched+1] = r
509 --- Create a function-call dispatching target.
510 -- @param name Target function of local controller
511 -- @param ... Additional parameters passed to the function
512 function call(name, ...)
516 return getfenv()[name](unpack(argv), ...)
518 return getfenv()[name](...)
523 --- Create a template render dispatching target.
524 -- @param name Template to be rendered
525 function template(name)
527 require("luci.template")
528 luci.template.render(name)
532 --- Create a CBI model dispatching target.
533 -- @param model CBI model to be rendered
534 function cbi(model, config)
535 config = config or {}
538 require("luci.template")
539 local http = require "luci.http"
541 maps = luci.cbi.load(model, ...)
545 for i, res in ipairs(maps) do
546 if config.autoapply then
547 res.autoapply = config.autoapply
549 local cstate = res:parse()
550 if not state or cstate < state then
555 if config.on_valid_to and state and state > 0 and state < 2 then
556 luci.http.redirect(config.on_valid_to)
560 if config.on_changed_to and state and state > 1 then
561 luci.http.redirect(config.on_changed_to)
565 if config.on_success_to and state and state > 0 then
566 luci.http.redirect(config.on_success_to)
570 if config.state_handler then
571 if not config.state_handler(state, maps) then
576 local pageaction = true
577 http.header("X-CBI-State", state or 0)
578 luci.template.render("cbi/header", {state = state})
579 for i, res in ipairs(maps) do
581 if res.pageaction == false then
585 luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
589 --- Create a CBI form model dispatching target.
590 -- @param model CBI form model tpo be rendered
594 require("luci.template")
595 local http = require "luci.http"
597 maps = luci.cbi.load(model, ...)
601 for i, res in ipairs(maps) do
602 local cstate = res:parse()
603 if not state or cstate < state then
608 http.header("X-CBI-State", state or 0)
609 luci.template.render("header")
610 for i, res in ipairs(maps) do
613 luci.template.render("footer")