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
151 for i, s in ipairs(request) do
154 tkey, tval = s:match(";(%w+)=(.*)")
169 util.update(track, c)
178 for j=n+1, #request do
179 args[#args+1] = request[j]
180 freq[#freq+1] = request[j]
184 ctx.requestpath = freq
188 require("luci.i18n").loadc(track.i18n)
191 -- Init template engine
192 if (c and c.index) or not track.notemplate then
193 local tpl = require("luci.template")
194 local media = track.mediaurlbase or luci.config.main.mediaurlbase
195 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
197 for name, theme in pairs(luci.config.themes) do
198 if name:sub(1,1) ~= "." and pcall(tpl.Template,
199 "themes/%s/header" % fs.basename(theme)) then
203 assert(media, "No valid theme found")
206 local viewns = setmetatable({}, {__index=function(table, key)
207 if key == "controller" then
209 elseif key == "REQUEST_URI" then
210 return build_url(unpack(ctx.requestpath))
212 return rawget(table, key) or _G[key]
215 tpl.context.viewns = viewns
216 viewns.write = luci.http.write
217 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
218 viewns.translate = function(...) return require("luci.i18n").translate(...) end
219 viewns.striptags = util.striptags
221 viewns.theme = fs.basename(media)
222 viewns.resource = luci.config.main.resourcebase
225 track.dependent = (track.dependent ~= false)
226 assert(not track.dependent or not track.auto, "Access Violation")
228 if track.sysauth then
229 local sauth = require "luci.sauth"
231 local authen = type(track.sysauth_authenticator) == "function"
232 and track.sysauth_authenticator
233 or authenticator[track.sysauth_authenticator]
235 local def = (type(track.sysauth) == "string") and track.sysauth
236 local accs = def and {track.sysauth} or track.sysauth
237 local sess = ctx.authsession
238 local verifytoken = false
240 sess = luci.http.getcookie("sysauth")
241 sess = sess and sess:match("^[A-F0-9]+$")
245 local sdat = sauth.read(sess)
249 sdat = loadstring(sdat)()
250 if not verifytoken or ctx.urltoken.stok == sdat.token then
255 if not util.contains(accs, user) then
257 ctx.urltoken.stok = nil
258 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
259 if not user or not util.contains(accs, user) then
262 local sid = sess or luci.sys.uniqueid(16)
264 local token = luci.sys.uniqueid(16)
265 sauth.write(sid, util.get_bytecode({
268 secret=luci.sys.uniqueid(16)
270 ctx.urltoken.stok = token
272 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
273 ctx.authsession = sid
276 luci.http.status(403, "Forbidden")
280 ctx.authsession = sess
284 if track.setgroup then
285 luci.sys.process.setgroup(track.setgroup)
288 if track.setuser then
289 luci.sys.process.setuser(track.setuser)
294 if type(c.target) == "function" then
296 elseif type(c.target) == "table" then
297 target = c.target.target
301 if c and (c.index or type(target) == "function") then
303 ctx.requested = ctx.requested or ctx.dispatched
306 if c and c.index then
307 local tpl = require "luci.template"
309 if util.copcall(tpl.render, "indexer", {}) then
314 if type(target) == "function" then
315 util.copcall(function()
316 local oldenv = getfenv(target)
317 local module = require(c.module)
318 local env = setmetatable({}, {__index=
321 return rawget(tbl, key) or module[key] or oldenv[key]
327 if type(c.target) == "table" then
328 target(c.target, unpack(args))
337 --- Generate the dispatching index using the best possible strategy.
338 function createindex()
339 local path = luci.util.libpath() .. "/controller/"
342 if luci.util.copcall(require, "luci.fastindex") then
343 createindex_fastindex(path, suff)
345 createindex_plain(path, suff)
349 --- Generate the dispatching index using the fastindex C-indexer.
350 -- @param path Controller base directory
351 -- @param suffix Controller file suffix
352 function createindex_fastindex(path, suffix)
356 fi = luci.fastindex.new("index")
357 fi.add(path .. "*" .. suffix)
358 fi.add(path .. "*/*" .. suffix)
362 for k, v in pairs(fi.indexes) do
367 --- Generate the dispatching index using the native file-cache based strategy.
368 -- @param path Controller base directory
369 -- @param suffix Controller file suffix
370 function createindex_plain(path, suffix)
371 local controllers = util.combine(
372 luci.fs.glob(path .. "*" .. suffix) or {},
373 luci.fs.glob(path .. "*/*" .. suffix) or {}
377 local cachedate = fs.mtime(indexcache)
380 for _, obj in ipairs(controllers) do
381 local omtime = fs.mtime(path .. "/" .. obj)
382 realdate = (omtime and omtime > realdate) and omtime or realdate
385 if cachedate > realdate then
387 sys.process.info("uid") == fs.stat(indexcache, "uid")
388 and fs.stat(indexcache, "mode") == "rw-------",
389 "Fatal: Indexcache is not sane!"
392 index = loadfile(indexcache)()
400 for i,c in ipairs(controllers) do
401 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
402 local mod = require(module)
403 local idx = mod.index
405 if type(idx) == "function" then
411 fs.writefile(indexcache, util.get_bytecode(index))
412 fs.chmod(indexcache, "a-rwx,u+rw")
416 --- Create the dispatching tree from the index.
417 -- Build the index before if it does not exist yet.
418 function createtree()
424 local tree = {nodes={}}
427 ctx.treecache = setmetatable({}, {__mode="v"})
431 -- Load default translation
432 require "luci.i18n".loadc("default")
434 local scope = setmetatable({}, {__index = luci.dispatcher})
436 for k, v in pairs(index) do
442 local function modisort(a,b)
443 return modi[a].order < modi[b].order
446 for _, v in util.spairs(modi, modisort) do
447 scope._NAME = v.module
448 setfenv(v.func, scope)
455 --- Register a tree modifier.
456 -- @param func Modifier function
457 -- @param order Modifier order value (optional)
458 function modifier(func, order)
459 context.modifiers[#context.modifiers+1] = {
467 --- Clone a node of the dispatching tree to another position.
468 -- @param path Virtual path destination
469 -- @param clone Virtual path source
470 -- @param title Destination node title (optional)
471 -- @param order Destination node order value (optional)
472 -- @return Dispatching tree node
473 function assign(path, clone, title, order)
474 local obj = node(unpack(path))
481 setmetatable(obj, {__index = _create_node(clone)})
486 --- Create a new dispatching node and define common parameters.
487 -- @param path Virtual path
488 -- @param target Target function to call when dispatched.
489 -- @param title Destination node title
490 -- @param order Destination node order value (optional)
491 -- @return Dispatching tree node
492 function entry(path, target, title, order)
493 local c = node(unpack(path))
498 c.module = getfenv(2)._NAME
503 --- Fetch or create a new dispatching node.
504 -- @param ... Virtual path
505 -- @return Dispatching tree node
507 local c = _create_node({...})
509 c.module = getfenv(2)._NAME
515 function _create_node(path, cache)
520 cache = cache or context.treecache
521 local name = table.concat(path, ".")
522 local c = cache[name]
525 local new = {nodes={}, auto=true, path=util.clone(path)}
526 local last = table.remove(path)
528 c = _create_node(path, cache)
541 --- Create a redirect to another dispatching node.
542 -- @param ... Virtual path destination
546 for _, r in ipairs({...}) do
554 --- Rewrite the first x path values of the request.
555 -- @param n Number of path values to replace
556 -- @param ... Virtual path to replace removed path values with
557 function rewrite(n, ...)
560 local dispatched = util.clone(context.dispatched)
563 table.remove(dispatched, 1)
566 for i, r in ipairs(req) do
567 table.insert(dispatched, i, r)
570 for _, r in ipairs({...}) do
571 dispatched[#dispatched+1] = r
579 local function _call(self, ...)
580 if #self.argv > 0 then
581 return getfenv()[self.name](unpack(self.argv), ...)
583 return getfenv()[self.name](...)
587 --- Create a function-call dispatching target.
588 -- @param name Target function of local controller
589 -- @param ... Additional parameters passed to the function
590 function call(name, ...)
591 return {type = "call", argv = {...}, name = name, target = _call}
595 local _template = function(self, ...)
596 require "luci.template".render(self.view)
599 --- Create a template render dispatching target.
600 -- @param name Template to be rendered
601 function template(name)
602 return {type = "template", view = name, target = _template}
606 local function _cbi(self, ...)
607 local cbi = require "luci.cbi"
608 local tpl = require "luci.template"
609 local http = require "luci.http"
611 local config = self.config or {}
612 local maps = cbi.load(self.model, ...)
616 for i, res in ipairs(maps) do
617 if config.autoapply then
618 res.autoapply = config.autoapply
620 local cstate = res:parse()
621 if not state or cstate < state then
626 if config.on_valid_to and state and state > 0 and state < 2 then
627 http.redirect(config.on_valid_to)
631 if config.on_changed_to and state and state > 1 then
632 http.redirect(config.on_changed_to)
636 if config.on_success_to and state and state > 0 then
637 http.redirect(config.on_success_to)
641 if config.state_handler then
642 if not config.state_handler(state, maps) then
647 local pageaction = true
648 http.header("X-CBI-State", state or 0)
649 tpl.render("cbi/header", {state = state})
650 for i, res in ipairs(maps) do
652 if res.pageaction == false then
656 tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
659 --- Create a CBI model dispatching target.
660 -- @param model CBI model to be rendered
661 function cbi(model, config)
662 return {type = "cbi", config = config, model = model, target = _cbi}
666 local function _arcombine(self, ...)
668 local target = #argv > 0 and self.targets[2] or self.targets[1]
669 setfenv(target.target, self.env)
670 target:target(unpack(argv))
673 --- Create a combined dispatching target for non argv and argv requests.
674 -- @param trg1 Overview Target
675 -- @param trg2 Detail Target
676 function arcombine(trg1, trg2)
677 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
681 local function _form(self, ...)
682 local cbi = require "luci.cbi"
683 local tpl = require "luci.template"
684 local http = require "luci.http"
686 local maps = luci.cbi.load(self.model, ...)
689 for i, res in ipairs(maps) do
690 local cstate = res:parse()
691 if not state or cstate < state then
696 http.header("X-CBI-State", state or 0)
698 for i, res in ipairs(maps) do
704 --- Create a CBI form model dispatching target.
705 -- @param model CBI form model tpo be rendered
707 return {type = "cbi", model = model, target = _form}