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 if not context.template_header_sent then
78 luci.http.status(500, "Internal Server Error")
80 require("luci.template")
81 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
82 luci.http.prepare_content("text/plain")
83 luci.http.write(message)
89 function authenticator.htmlauth(validator, accs, default)
90 local user = luci.http.formvalue("username")
91 local pass = luci.http.formvalue("password")
93 if user and validator(user, pass) then
98 require("luci.template")
100 luci.template.render("sysauth", {duser=default, fuser=user})
105 --- Dispatch an HTTP request.
106 -- @param request LuCI HTTP Request object
107 function httpdispatch(request)
108 luci.http.context.request = request
110 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
112 for node in pathinfo:gmatch("[^/]+") do
113 table.insert(context.request, node)
116 local stat, err = util.copcall(dispatch, context.request)
118 luci.util.perror(err)
124 --context._disable_memtrace()
127 --- Dispatches a LuCI virtual path.
128 -- @param request Virtual path
129 function dispatch(request)
130 --context._disable_memtrace = require "luci.debug".trap_memtrace()
133 ctx.urltoken = ctx.urltoken or {}
135 local conf = require "luci.config"
136 local lang = conf.main.lang
137 if lang == "auto" then
138 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
139 for lpat in aclang:gmatch("[%w-]+") do
140 lpat = lpat and lpat:gsub("-", "_")
141 if conf.languages[lpat] then
147 require "luci.i18n".setlanguage(lang)
158 ctx.requestargs = ctx.requestargs or args
161 local token = ctx.urltoken
165 for i, s in ipairs(request) do
168 tkey, tval = s:match(";(%w+)=(.*)")
183 util.update(track, c)
192 for j=n+1, #request do
193 args[#args+1] = request[j]
194 freq[#freq+1] = request[j]
198 ctx.requestpath = freq
202 require("luci.i18n").loadc(track.i18n)
205 -- Init template engine
206 if (c and c.index) or not track.notemplate then
207 local tpl = require("luci.template")
208 local media = track.mediaurlbase or luci.config.main.mediaurlbase
209 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
211 for name, theme in pairs(luci.config.themes) do
212 if name:sub(1,1) ~= "." and pcall(tpl.Template,
213 "themes/%s/header" % fs.basename(theme)) then
217 assert(media, "No valid theme found")
220 local viewns = setmetatable({}, {__index=function(table, key)
221 if key == "controller" then
223 elseif key == "REQUEST_URI" then
224 return build_url(unpack(ctx.requestpath))
226 return rawget(table, key) or _G[key]
229 tpl.context.viewns = viewns
230 viewns.write = luci.http.write
231 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
232 viewns.translate = function(...) return require("luci.i18n").translate(...) end
233 viewns.striptags = util.striptags
235 viewns.theme = fs.basename(media)
236 viewns.resource = luci.config.main.resourcebase
239 track.dependent = (track.dependent ~= false)
240 assert(not track.dependent or not track.auto, "Access Violation")
242 if track.sysauth then
243 local sauth = require "luci.sauth"
245 local authen = type(track.sysauth_authenticator) == "function"
246 and track.sysauth_authenticator
247 or authenticator[track.sysauth_authenticator]
249 local def = (type(track.sysauth) == "string") and track.sysauth
250 local accs = def and {track.sysauth} or track.sysauth
251 local sess = ctx.authsession
252 local verifytoken = false
254 sess = luci.http.getcookie("sysauth")
255 sess = sess and sess:match("^[A-F0-9]+$")
259 local sdat = sauth.read(sess)
263 sdat = loadstring(sdat)()
264 if not verifytoken or ctx.urltoken.stok == sdat.token then
269 if not util.contains(accs, user) then
271 ctx.urltoken.stok = nil
272 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
273 if not user or not util.contains(accs, user) then
276 local sid = sess or luci.sys.uniqueid(16)
278 local token = luci.sys.uniqueid(16)
279 sauth.write(sid, util.get_bytecode({
282 secret=luci.sys.uniqueid(16)
284 ctx.urltoken.stok = token
286 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
287 ctx.authsession = sid
290 luci.http.status(403, "Forbidden")
294 ctx.authsession = sess
298 if track.setgroup then
299 luci.sys.process.setgroup(track.setgroup)
302 if track.setuser then
303 luci.sys.process.setuser(track.setuser)
308 if type(c.target) == "function" then
310 elseif type(c.target) == "table" then
311 target = c.target.target
315 if c and (c.index or type(target) == "function") then
317 ctx.requested = ctx.requested or ctx.dispatched
320 if c and c.index then
321 local tpl = require "luci.template"
323 if util.copcall(tpl.render, "indexer", {}) then
328 if type(target) == "function" then
329 util.copcall(function()
330 local oldenv = getfenv(target)
331 local module = require(c.module)
332 local env = setmetatable({}, {__index=
335 return rawget(tbl, key) or module[key] or oldenv[key]
341 if type(c.target) == "table" then
342 target(c.target, unpack(args))
351 --- Generate the dispatching index using the best possible strategy.
352 function createindex()
353 local path = luci.util.libpath() .. "/controller/"
356 if luci.util.copcall(require, "luci.fastindex") then
357 createindex_fastindex(path, suff)
359 createindex_plain(path, suff)
363 --- Generate the dispatching index using the fastindex C-indexer.
364 -- @param path Controller base directory
365 -- @param suffix Controller file suffix
366 function createindex_fastindex(path, suffix)
370 fi = luci.fastindex.new("index")
371 fi.add(path .. "*" .. suffix)
372 fi.add(path .. "*/*" .. suffix)
376 for k, v in pairs(fi.indexes) do
381 --- Generate the dispatching index using the native file-cache based strategy.
382 -- @param path Controller base directory
383 -- @param suffix Controller file suffix
384 function createindex_plain(path, suffix)
385 local controllers = util.combine(
386 luci.fs.glob(path .. "*" .. suffix) or {},
387 luci.fs.glob(path .. "*/*" .. suffix) or {}
391 local cachedate = fs.mtime(indexcache)
394 for _, obj in ipairs(controllers) do
395 local omtime = fs.mtime(path .. "/" .. obj)
396 realdate = (omtime and omtime > realdate) and omtime or realdate
399 if cachedate > realdate then
401 sys.process.info("uid") == fs.stat(indexcache, "uid")
402 and fs.stat(indexcache, "mode") == "rw-------",
403 "Fatal: Indexcache is not sane!"
406 index = loadfile(indexcache)()
414 for i,c in ipairs(controllers) do
415 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
416 local mod = require(module)
417 local idx = mod.index
419 if type(idx) == "function" then
425 fs.writefile(indexcache, util.get_bytecode(index))
426 fs.chmod(indexcache, "a-rwx,u+rw")
430 --- Create the dispatching tree from the index.
431 -- Build the index before if it does not exist yet.
432 function createtree()
438 local tree = {nodes={}}
441 ctx.treecache = setmetatable({}, {__mode="v"})
445 -- Load default translation
446 require "luci.i18n".loadc("default")
448 local scope = setmetatable({}, {__index = luci.dispatcher})
450 for k, v in pairs(index) do
456 local function modisort(a,b)
457 return modi[a].order < modi[b].order
460 for _, v in util.spairs(modi, modisort) do
461 scope._NAME = v.module
462 setfenv(v.func, scope)
469 --- Register a tree modifier.
470 -- @param func Modifier function
471 -- @param order Modifier order value (optional)
472 function modifier(func, order)
473 context.modifiers[#context.modifiers+1] = {
481 --- Clone a node of the dispatching tree to another position.
482 -- @param path Virtual path destination
483 -- @param clone Virtual path source
484 -- @param title Destination node title (optional)
485 -- @param order Destination node order value (optional)
486 -- @return Dispatching tree node
487 function assign(path, clone, title, order)
488 local obj = node(unpack(path))
495 setmetatable(obj, {__index = _create_node(clone)})
500 --- Create a new dispatching node and define common parameters.
501 -- @param path Virtual path
502 -- @param target Target function to call when dispatched.
503 -- @param title Destination node title
504 -- @param order Destination node order value (optional)
505 -- @return Dispatching tree node
506 function entry(path, target, title, order)
507 local c = node(unpack(path))
512 c.module = getfenv(2)._NAME
517 --- Fetch or create a new dispatching node.
518 -- @param ... Virtual path
519 -- @return Dispatching tree node
521 local c = _create_node({...})
523 c.module = getfenv(2)._NAME
529 function _create_node(path, cache)
534 cache = cache or context.treecache
535 local name = table.concat(path, ".")
536 local c = cache[name]
539 local new = {nodes={}, auto=true, path=util.clone(path)}
540 local last = table.remove(path)
542 c = _create_node(path, cache)
555 --- Create a redirect to another dispatching node.
556 -- @param ... Virtual path destination
560 for _, r in ipairs({...}) do
568 --- Rewrite the first x path values of the request.
569 -- @param n Number of path values to replace
570 -- @param ... Virtual path to replace removed path values with
571 function rewrite(n, ...)
574 local dispatched = util.clone(context.dispatched)
577 table.remove(dispatched, 1)
580 for i, r in ipairs(req) do
581 table.insert(dispatched, i, r)
584 for _, r in ipairs({...}) do
585 dispatched[#dispatched+1] = r
593 local function _call(self, ...)
594 if #self.argv > 0 then
595 return getfenv()[self.name](unpack(self.argv), ...)
597 return getfenv()[self.name](...)
601 --- Create a function-call dispatching target.
602 -- @param name Target function of local controller
603 -- @param ... Additional parameters passed to the function
604 function call(name, ...)
605 return {type = "call", argv = {...}, name = name, target = _call}
609 local _template = function(self, ...)
610 require "luci.template".render(self.view)
613 --- Create a template render dispatching target.
614 -- @param name Template to be rendered
615 function template(name)
616 return {type = "template", view = name, target = _template}
620 local function _cbi(self, ...)
621 local cbi = require "luci.cbi"
622 local tpl = require "luci.template"
623 local http = require "luci.http"
625 local config = self.config or {}
626 local maps = cbi.load(self.model, ...)
630 for i, res in ipairs(maps) do
631 if config.autoapply then
632 res.autoapply = config.autoapply
634 local cstate = res:parse()
635 if not state or cstate < state then
640 if config.on_valid_to and state and state > 0 and state < 2 then
641 http.redirect(config.on_valid_to)
645 if config.on_changed_to and state and state > 1 then
646 http.redirect(config.on_changed_to)
650 if config.on_success_to and state and state > 0 then
651 http.redirect(config.on_success_to)
655 if config.state_handler then
656 if not config.state_handler(state, maps) then
661 local pageaction = true
662 http.header("X-CBI-State", state or 0)
663 tpl.render("cbi/header", {state = state})
664 for i, res in ipairs(maps) do
666 if res.pageaction == false then
670 tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
673 --- Create a CBI model dispatching target.
674 -- @param model CBI model to be rendered
675 function cbi(model, config)
676 return {type = "cbi", config = config, model = model, target = _cbi}
680 local function _arcombine(self, ...)
682 local target = #argv > 0 and self.targets[2] or self.targets[1]
683 setfenv(target.target, self.env)
684 target:target(unpack(argv))
687 --- Create a combined dispatching target for non argv and argv requests.
688 -- @param trg1 Overview Target
689 -- @param trg2 Detail Target
690 function arcombine(trg1, trg2)
691 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
695 local function _form(self, ...)
696 local cbi = require "luci.cbi"
697 local tpl = require "luci.template"
698 local http = require "luci.http"
700 local maps = luci.cbi.load(self.model, ...)
703 for i, res in ipairs(maps) do
704 local cstate = res:parse()
705 if not state or cstate < state then
710 http.header("X-CBI-State", state or 0)
712 for i, res in ipairs(maps) do
718 --- Create a CBI form model dispatching target.
719 -- @param model CBI form model tpo be rendered
721 return {type = "cbi", model = model, target = _form}