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")
80 luci.http.prepare_content("text/plain")
81 luci.http.write(message)
83 require("luci.template")
84 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
85 luci.http.prepare_content("text/plain")
86 luci.http.write(message)
92 function authenticator.htmlauth(validator, accs, default)
93 local user = luci.http.formvalue("username")
94 local pass = luci.http.formvalue("password")
96 if user and validator(user, pass) then
101 require("luci.template")
103 luci.template.render("sysauth", {duser=default, fuser=user})
108 --- Dispatch an HTTP request.
109 -- @param request LuCI HTTP Request object
110 function httpdispatch(request)
111 luci.http.context.request = request
113 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
115 for node in pathinfo:gmatch("[^/]+") do
116 table.insert(context.request, node)
119 local stat, err = util.coxpcall(function()
120 dispatch(context.request)
125 --context._disable_memtrace()
128 --- Dispatches a LuCI virtual path.
129 -- @param request Virtual path
130 function dispatch(request)
131 --context._disable_memtrace = require "luci.debug".trap_memtrace()
134 ctx.urltoken = ctx.urltoken or {}
136 local conf = require "luci.config"
138 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
140 local lang = conf.main.lang
141 if lang == "auto" then
142 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
143 for lpat in aclang:gmatch("[%w-]+") do
144 lpat = lpat and lpat:gsub("-", "_")
145 if conf.languages[lpat] then
151 require "luci.i18n".setlanguage(lang)
162 ctx.requestargs = ctx.requestargs or args
165 local token = ctx.urltoken
169 for i, s in ipairs(request) do
172 tkey, tval = s:match(";(%w+)=(.*)")
187 util.update(track, c)
196 for j=n+1, #request do
197 args[#args+1] = request[j]
198 freq[#freq+1] = request[j]
202 ctx.requestpath = freq
206 require("luci.i18n").loadc(track.i18n)
209 -- Init template engine
210 if (c and c.index) or not track.notemplate then
211 local tpl = require("luci.template")
212 local media = track.mediaurlbase or luci.config.main.mediaurlbase
213 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
215 for name, theme in pairs(luci.config.themes) do
216 if name:sub(1,1) ~= "." and pcall(tpl.Template,
217 "themes/%s/header" % fs.basename(theme)) then
221 assert(media, "No valid theme found")
224 local viewns = setmetatable({}, {__index=function(table, key)
225 if key == "controller" then
227 elseif key == "REQUEST_URI" then
228 return build_url(unpack(ctx.requestpath))
230 return rawget(table, key) or _G[key]
233 tpl.context.viewns = viewns
234 viewns.write = luci.http.write
235 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
236 viewns.translate = function(...) return require("luci.i18n").translate(...) end
237 viewns.striptags = util.striptags
239 viewns.theme = fs.basename(media)
240 viewns.resource = luci.config.main.resourcebase
243 track.dependent = (track.dependent ~= false)
244 assert(not track.dependent or not track.auto, "Access Violation")
246 if track.sysauth then
247 local sauth = require "luci.sauth"
249 local authen = type(track.sysauth_authenticator) == "function"
250 and track.sysauth_authenticator
251 or authenticator[track.sysauth_authenticator]
253 local def = (type(track.sysauth) == "string") and track.sysauth
254 local accs = def and {track.sysauth} or track.sysauth
255 local sess = ctx.authsession
256 local verifytoken = false
258 sess = luci.http.getcookie("sysauth")
259 sess = sess and sess:match("^[A-F0-9]+$")
263 local sdat = sauth.read(sess)
267 sdat = loadstring(sdat)
270 if not verifytoken or ctx.urltoken.stok == sdat.token then
275 if not util.contains(accs, user) then
277 ctx.urltoken.stok = nil
278 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
279 if not user or not util.contains(accs, user) then
282 local sid = sess or luci.sys.uniqueid(16)
284 local token = luci.sys.uniqueid(16)
285 sauth.write(sid, util.get_bytecode({
288 secret=luci.sys.uniqueid(16)
290 ctx.urltoken.stok = token
292 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
293 ctx.authsession = sid
296 luci.http.status(403, "Forbidden")
300 ctx.authsession = sess
304 if track.setgroup then
305 luci.sys.process.setgroup(track.setgroup)
308 if track.setuser then
309 luci.sys.process.setuser(track.setuser)
314 if type(c.target) == "function" then
316 elseif type(c.target) == "table" then
317 target = c.target.target
321 if c and (c.index or type(target) == "function") then
323 ctx.requested = ctx.requested or ctx.dispatched
326 if c and c.index then
327 local tpl = require "luci.template"
329 if util.copcall(tpl.render, "indexer", {}) then
334 if type(target) == "function" then
335 util.copcall(function()
336 local oldenv = getfenv(target)
337 local module = require(c.module)
338 local env = setmetatable({}, {__index=
341 return rawget(tbl, key) or module[key] or oldenv[key]
347 if type(c.target) == "table" then
348 target(c.target, unpack(args))
357 --- Generate the dispatching index using the best possible strategy.
358 function createindex()
359 local path = luci.util.libpath() .. "/controller/"
360 local suff = { ".lua", ".lua.gz" }
362 if luci.util.copcall(require, "luci.fastindex") then
363 createindex_fastindex(path, suff)
365 createindex_plain(path, suff)
369 --- Generate the dispatching index using the fastindex C-indexer.
370 -- @param path Controller base directory
371 -- @param suffixes Controller file suffixes
372 function createindex_fastindex(path, suffixes)
376 fi = luci.fastindex.new("index")
377 for _, suffix in ipairs(suffixes) do
378 fi.add(path .. "*" .. suffix)
379 fi.add(path .. "*/*" .. suffix)
384 for k, v in pairs(fi.indexes) do
389 --- Generate the dispatching index using the native file-cache based strategy.
390 -- @param path Controller base directory
391 -- @param suffixes Controller file suffixes
392 function createindex_plain(path, suffixes)
393 local controllers = { }
394 for _, suffix in ipairs(suffixes) do
395 controllers = util.combine(
397 luci.fs.glob(path .. "*" .. suffix) or {},
398 luci.fs.glob(path .. "*/*" .. suffix) or {}
403 local cachedate = fs.mtime(indexcache)
406 for _, obj in ipairs(controllers) do
407 local omtime = fs.mtime(path .. "/" .. obj)
408 realdate = (omtime and omtime > realdate) and omtime or realdate
411 if cachedate > realdate then
413 sys.process.info("uid") == fs.stat(indexcache, "uid")
414 and fs.stat(indexcache, "mode") == "rw-------",
415 "Fatal: Indexcache is not sane!"
418 index = loadfile(indexcache)()
426 for i,c in ipairs(controllers) do
427 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
428 for _, suffix in ipairs(suffixes) do
429 module = module:gsub(suffix.."$", "")
432 local mod = require(module)
433 local idx = mod.index
435 if type(idx) == "function" then
441 fs.writefile(indexcache, util.get_bytecode(index))
442 fs.chmod(indexcache, "a-rwx,u+rw")
446 --- Create the dispatching tree from the index.
447 -- Build the index before if it does not exist yet.
448 function createtree()
454 local tree = {nodes={}}
457 ctx.treecache = setmetatable({}, {__mode="v"})
461 -- Load default translation
462 require "luci.i18n".loadc("default")
464 local scope = setmetatable({}, {__index = luci.dispatcher})
466 for k, v in pairs(index) do
472 local function modisort(a,b)
473 return modi[a].order < modi[b].order
476 for _, v in util.spairs(modi, modisort) do
477 scope._NAME = v.module
478 setfenv(v.func, scope)
485 --- Register a tree modifier.
486 -- @param func Modifier function
487 -- @param order Modifier order value (optional)
488 function modifier(func, order)
489 context.modifiers[#context.modifiers+1] = {
497 --- Clone a node of the dispatching tree to another position.
498 -- @param path Virtual path destination
499 -- @param clone Virtual path source
500 -- @param title Destination node title (optional)
501 -- @param order Destination node order value (optional)
502 -- @return Dispatching tree node
503 function assign(path, clone, title, order)
504 local obj = node(unpack(path))
511 setmetatable(obj, {__index = _create_node(clone)})
516 --- Create a new dispatching node and define common parameters.
517 -- @param path Virtual path
518 -- @param target Target function to call when dispatched.
519 -- @param title Destination node title
520 -- @param order Destination node order value (optional)
521 -- @return Dispatching tree node
522 function entry(path, target, title, order)
523 local c = node(unpack(path))
528 c.module = getfenv(2)._NAME
533 --- Fetch or create a dispatching node without setting the target module or
534 -- enabling the node.
535 -- @param ... Virtual path
536 -- @return Dispatching tree node
538 return _create_node({...})
541 --- Fetch or create a new dispatching node.
542 -- @param ... Virtual path
543 -- @return Dispatching tree node
545 local c = _create_node({...})
547 c.module = getfenv(2)._NAME
553 function _create_node(path, cache)
558 cache = cache or context.treecache
559 local name = table.concat(path, ".")
560 local c = cache[name]
563 local new = {nodes={}, auto=true, path=util.clone(path)}
564 local last = table.remove(path)
566 c = _create_node(path, cache)
579 --- Create a redirect to another dispatching node.
580 -- @param ... Virtual path destination
584 for _, r in ipairs({...}) do
592 --- Rewrite the first x path values of the request.
593 -- @param n Number of path values to replace
594 -- @param ... Virtual path to replace removed path values with
595 function rewrite(n, ...)
598 local dispatched = util.clone(context.dispatched)
601 table.remove(dispatched, 1)
604 for i, r in ipairs(req) do
605 table.insert(dispatched, i, r)
608 for _, r in ipairs({...}) do
609 dispatched[#dispatched+1] = r
617 local function _call(self, ...)
618 if #self.argv > 0 then
619 return getfenv()[self.name](unpack(self.argv), ...)
621 return getfenv()[self.name](...)
625 --- Create a function-call dispatching target.
626 -- @param name Target function of local controller
627 -- @param ... Additional parameters passed to the function
628 function call(name, ...)
629 return {type = "call", argv = {...}, name = name, target = _call}
633 local _template = function(self, ...)
634 require "luci.template".render(self.view)
637 --- Create a template render dispatching target.
638 -- @param name Template to be rendered
639 function template(name)
640 return {type = "template", view = name, target = _template}
644 local function _cbi(self, ...)
645 local cbi = require "luci.cbi"
646 local tpl = require "luci.template"
647 local http = require "luci.http"
649 local config = self.config or {}
650 local maps = cbi.load(self.model, ...)
654 for i, res in ipairs(maps) do
656 local cstate = res:parse()
657 if cstate and (not state or cstate < state) then
662 local function _resolve_path(path)
663 return type(path) == "table" and build_url(unpack(path)) or path
666 if config.on_valid_to and state and state > 0 and state < 2 then
667 http.redirect(_resolve_path(config.on_valid_to))
671 if config.on_changed_to and state and state > 1 then
672 http.redirect(_resolve_path(config.on_changed_to))
676 if config.on_success_to and state and state > 0 then
677 http.redirect(_resolve_path(config.on_success_to))
681 if config.state_handler then
682 if not config.state_handler(state, maps) then
687 local pageaction = true
688 http.header("X-CBI-State", state or 0)
689 if not config.noheader then
690 tpl.render("cbi/header", {state = state})
692 for i, res in ipairs(maps) do
694 if res.pageaction == false then
698 if not config.nofooter then
699 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
703 --- Create a CBI model dispatching target.
704 -- @param model CBI model to be rendered
705 function cbi(model, config)
706 return {type = "cbi", config = config, model = model, target = _cbi}
710 local function _arcombine(self, ...)
712 local target = #argv > 0 and self.targets[2] or self.targets[1]
713 setfenv(target.target, self.env)
714 target:target(unpack(argv))
717 --- Create a combined dispatching target for non argv and argv requests.
718 -- @param trg1 Overview Target
719 -- @param trg2 Detail Target
720 function arcombine(trg1, trg2)
721 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
725 local function _form(self, ...)
726 local cbi = require "luci.cbi"
727 local tpl = require "luci.template"
728 local http = require "luci.http"
730 local maps = luci.cbi.load(self.model, ...)
733 for i, res in ipairs(maps) do
734 local cstate = res:parse()
735 if cstate and (not state or cstate < state) then
740 http.header("X-CBI-State", state or 0)
742 for i, res in ipairs(maps) do
748 --- Create a CBI form model dispatching target.
749 -- @param model CBI form model tpo be rendered
751 return {type = "cbi", model = model, target = _form}