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")
81 require("luci.template")
82 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
83 luci.http.prepare_content("text/plain")
84 luci.http.write(message)
90 function authenticator.htmlauth(validator, accs, default)
91 local user = luci.http.formvalue("username")
92 local pass = luci.http.formvalue("password")
94 if user and validator(user, pass) then
99 require("luci.template")
101 luci.template.render("sysauth", {duser=default, fuser=user})
106 --- Dispatch an HTTP request.
107 -- @param request LuCI HTTP Request object
108 function httpdispatch(request)
109 luci.http.context.request = request
111 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
113 for node in pathinfo:gmatch("[^/]+") do
114 table.insert(context.request, node)
117 local stat, err = util.coxpcall(function()
118 dispatch(context.request)
123 --context._disable_memtrace()
126 --- Dispatches a LuCI virtual path.
127 -- @param request Virtual path
128 function dispatch(request)
129 --context._disable_memtrace = require "luci.debug".trap_memtrace()
132 ctx.urltoken = ctx.urltoken or {}
134 local conf = require "luci.config"
135 local lang = conf.main.lang
136 if lang == "auto" then
137 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
138 for lpat in aclang:gmatch("[%w-]+") do
139 lpat = lpat and lpat:gsub("-", "_")
140 if conf.languages[lpat] then
146 require "luci.i18n".setlanguage(lang)
157 ctx.requestargs = ctx.requestargs or args
160 local token = ctx.urltoken
164 for i, s in ipairs(request) do
167 tkey, tval = s:match(";(%w+)=(.*)")
182 util.update(track, c)
191 for j=n+1, #request do
192 args[#args+1] = request[j]
193 freq[#freq+1] = request[j]
197 ctx.requestpath = freq
201 require("luci.i18n").loadc(track.i18n)
204 -- Init template engine
205 if (c and c.index) or not track.notemplate then
206 local tpl = require("luci.template")
207 local media = track.mediaurlbase or luci.config.main.mediaurlbase
208 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
210 for name, theme in pairs(luci.config.themes) do
211 if name:sub(1,1) ~= "." and pcall(tpl.Template,
212 "themes/%s/header" % fs.basename(theme)) then
216 assert(media, "No valid theme found")
219 local viewns = setmetatable({}, {__index=function(table, key)
220 if key == "controller" then
222 elseif key == "REQUEST_URI" then
223 return build_url(unpack(ctx.requestpath))
225 return rawget(table, key) or _G[key]
228 tpl.context.viewns = viewns
229 viewns.write = luci.http.write
230 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
231 viewns.translate = function(...) return require("luci.i18n").translate(...) end
232 viewns.striptags = util.striptags
234 viewns.theme = fs.basename(media)
235 viewns.resource = luci.config.main.resourcebase
238 track.dependent = (track.dependent ~= false)
239 assert(not track.dependent or not track.auto, "Access Violation")
241 if track.sysauth then
242 local sauth = require "luci.sauth"
244 local authen = type(track.sysauth_authenticator) == "function"
245 and track.sysauth_authenticator
246 or authenticator[track.sysauth_authenticator]
248 local def = (type(track.sysauth) == "string") and track.sysauth
249 local accs = def and {track.sysauth} or track.sysauth
250 local sess = ctx.authsession
251 local verifytoken = false
253 sess = luci.http.getcookie("sysauth")
254 sess = sess and sess:match("^[A-F0-9]+$")
258 local sdat = sauth.read(sess)
262 sdat = loadstring(sdat)()
263 if not verifytoken or ctx.urltoken.stok == sdat.token then
268 if not util.contains(accs, user) then
270 ctx.urltoken.stok = nil
271 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
272 if not user or not util.contains(accs, user) then
275 local sid = sess or luci.sys.uniqueid(16)
277 local token = luci.sys.uniqueid(16)
278 sauth.write(sid, util.get_bytecode({
281 secret=luci.sys.uniqueid(16)
283 ctx.urltoken.stok = token
285 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
286 ctx.authsession = sid
289 luci.http.status(403, "Forbidden")
293 ctx.authsession = sess
297 if track.setgroup then
298 luci.sys.process.setgroup(track.setgroup)
301 if track.setuser then
302 luci.sys.process.setuser(track.setuser)
307 if type(c.target) == "function" then
309 elseif type(c.target) == "table" then
310 target = c.target.target
314 if c and (c.index or type(target) == "function") then
316 ctx.requested = ctx.requested or ctx.dispatched
319 if c and c.index then
320 local tpl = require "luci.template"
322 if util.copcall(tpl.render, "indexer", {}) then
327 if type(target) == "function" then
328 util.copcall(function()
329 local oldenv = getfenv(target)
330 local module = require(c.module)
331 local env = setmetatable({}, {__index=
334 return rawget(tbl, key) or module[key] or oldenv[key]
340 if type(c.target) == "table" then
341 target(c.target, unpack(args))
350 --- Generate the dispatching index using the best possible strategy.
351 function createindex()
352 local path = luci.util.libpath() .. "/controller/"
355 if luci.util.copcall(require, "luci.fastindex") then
356 createindex_fastindex(path, suff)
358 createindex_plain(path, suff)
362 --- Generate the dispatching index using the fastindex C-indexer.
363 -- @param path Controller base directory
364 -- @param suffix Controller file suffix
365 function createindex_fastindex(path, suffix)
369 fi = luci.fastindex.new("index")
370 fi.add(path .. "*" .. suffix)
371 fi.add(path .. "*/*" .. suffix)
375 for k, v in pairs(fi.indexes) do
380 --- Generate the dispatching index using the native file-cache based strategy.
381 -- @param path Controller base directory
382 -- @param suffix Controller file suffix
383 function createindex_plain(path, suffix)
384 local controllers = util.combine(
385 luci.fs.glob(path .. "*" .. suffix) or {},
386 luci.fs.glob(path .. "*/*" .. suffix) or {}
390 local cachedate = fs.mtime(indexcache)
393 for _, obj in ipairs(controllers) do
394 local omtime = fs.mtime(path .. "/" .. obj)
395 realdate = (omtime and omtime > realdate) and omtime or realdate
398 if cachedate > realdate then
400 sys.process.info("uid") == fs.stat(indexcache, "uid")
401 and fs.stat(indexcache, "mode") == "rw-------",
402 "Fatal: Indexcache is not sane!"
405 index = loadfile(indexcache)()
413 for i,c in ipairs(controllers) do
414 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
415 local mod = require(module)
416 local idx = mod.index
418 if type(idx) == "function" then
424 fs.writefile(indexcache, util.get_bytecode(index))
425 fs.chmod(indexcache, "a-rwx,u+rw")
429 --- Create the dispatching tree from the index.
430 -- Build the index before if it does not exist yet.
431 function createtree()
437 local tree = {nodes={}}
440 ctx.treecache = setmetatable({}, {__mode="v"})
444 -- Load default translation
445 require "luci.i18n".loadc("default")
447 local scope = setmetatable({}, {__index = luci.dispatcher})
449 for k, v in pairs(index) do
455 local function modisort(a,b)
456 return modi[a].order < modi[b].order
459 for _, v in util.spairs(modi, modisort) do
460 scope._NAME = v.module
461 setfenv(v.func, scope)
468 --- Register a tree modifier.
469 -- @param func Modifier function
470 -- @param order Modifier order value (optional)
471 function modifier(func, order)
472 context.modifiers[#context.modifiers+1] = {
480 --- Clone a node of the dispatching tree to another position.
481 -- @param path Virtual path destination
482 -- @param clone Virtual path source
483 -- @param title Destination node title (optional)
484 -- @param order Destination node order value (optional)
485 -- @return Dispatching tree node
486 function assign(path, clone, title, order)
487 local obj = node(unpack(path))
494 setmetatable(obj, {__index = _create_node(clone)})
499 --- Create a new dispatching node and define common parameters.
500 -- @param path Virtual path
501 -- @param target Target function to call when dispatched.
502 -- @param title Destination node title
503 -- @param order Destination node order value (optional)
504 -- @return Dispatching tree node
505 function entry(path, target, title, order)
506 local c = node(unpack(path))
511 c.module = getfenv(2)._NAME
516 --- Fetch or create a dispatching node without setting the target module or
517 -- enabling the node.
518 -- @param ... Virtual path
519 -- @return Dispatching tree node
521 return _create_node({...})
524 --- Fetch or create a new dispatching node.
525 -- @param ... Virtual path
526 -- @return Dispatching tree node
528 local c = _create_node({...})
530 c.module = getfenv(2)._NAME
536 function _create_node(path, cache)
541 cache = cache or context.treecache
542 local name = table.concat(path, ".")
543 local c = cache[name]
546 local new = {nodes={}, auto=true, path=util.clone(path)}
547 local last = table.remove(path)
549 c = _create_node(path, cache)
562 --- Create a redirect to another dispatching node.
563 -- @param ... Virtual path destination
567 for _, r in ipairs({...}) do
575 --- Rewrite the first x path values of the request.
576 -- @param n Number of path values to replace
577 -- @param ... Virtual path to replace removed path values with
578 function rewrite(n, ...)
581 local dispatched = util.clone(context.dispatched)
584 table.remove(dispatched, 1)
587 for i, r in ipairs(req) do
588 table.insert(dispatched, i, r)
591 for _, r in ipairs({...}) do
592 dispatched[#dispatched+1] = r
600 local function _call(self, ...)
601 if #self.argv > 0 then
602 return getfenv()[self.name](unpack(self.argv), ...)
604 return getfenv()[self.name](...)
608 --- Create a function-call dispatching target.
609 -- @param name Target function of local controller
610 -- @param ... Additional parameters passed to the function
611 function call(name, ...)
612 return {type = "call", argv = {...}, name = name, target = _call}
616 local _template = function(self, ...)
617 require "luci.template".render(self.view)
620 --- Create a template render dispatching target.
621 -- @param name Template to be rendered
622 function template(name)
623 return {type = "template", view = name, target = _template}
627 local function _cbi(self, ...)
628 local cbi = require "luci.cbi"
629 local tpl = require "luci.template"
630 local http = require "luci.http"
632 local config = self.config or {}
633 local maps = cbi.load(self.model, ...)
637 for i, res in ipairs(maps) do
638 if config.autoapply then
639 res.autoapply = config.autoapply
641 local cstate = res:parse()
642 if cstate and not state or cstate < state then
647 if config.on_valid_to and state and state > 0 and state < 2 then
648 http.redirect(config.on_valid_to)
652 if config.on_changed_to and state and state > 1 then
653 http.redirect(config.on_changed_to)
657 if config.on_success_to and state and state > 0 then
658 http.redirect(config.on_success_to)
662 if config.state_handler then
663 if not config.state_handler(state, maps) then
668 local pageaction = true
669 http.header("X-CBI-State", state or 0)
670 tpl.render("cbi/header", {state = state})
671 for i, res in ipairs(maps) do
673 if res.pageaction == false then
677 tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
680 --- Create a CBI model dispatching target.
681 -- @param model CBI model to be rendered
682 function cbi(model, config)
683 return {type = "cbi", config = config, model = model, target = _cbi}
687 local function _arcombine(self, ...)
689 local target = #argv > 0 and self.targets[2] or self.targets[1]
690 setfenv(target.target, self.env)
691 target:target(unpack(argv))
694 --- Create a combined dispatching target for non argv and argv requests.
695 -- @param trg1 Overview Target
696 -- @param trg2 Detail Target
697 function arcombine(trg1, trg2)
698 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
702 local function _form(self, ...)
703 local cbi = require "luci.cbi"
704 local tpl = require "luci.template"
705 local http = require "luci.http"
707 local maps = luci.cbi.load(self.model, ...)
710 for i, res in ipairs(maps) do
711 local cstate = res:parse()
712 if cstate and not state or cstate < state then
717 http.header("X-CBI-State", state or 0)
719 for i, res in ipairs(maps) do
725 --- Create a CBI form model dispatching target.
726 -- @param model CBI form model tpo be rendered
728 return {type = "cbi", model = model, target = _form}