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 = http.urldecode(request:getenv("PATH_INFO") or "", true)
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 local conf = require "luci.config"
134 local lang = conf.main.lang
135 if lang == "auto" then
136 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
137 for lpat in aclang:gmatch("[%w]+") do
138 if conf.languages[lpat] then
144 require "luci.i18n".setlanguage(lang)
155 ctx.requestargs = ctx.requestargs or args
158 local token = ctx.urltoken
162 for i, s in ipairs(request) do
165 tkey, tval = s:match(";(%w+)=(.*)")
180 util.update(track, c)
189 for j=n+1, #request do
190 args[#args+1] = request[j]
191 freq[#freq+1] = request[j]
195 ctx.requestpath = freq
199 require("luci.i18n").loadc(track.i18n)
202 -- Init template engine
203 if (c and c.index) or not track.notemplate then
204 local tpl = require("luci.template")
205 local media = track.mediaurlbase or luci.config.main.mediaurlbase
206 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
208 for name, theme in pairs(luci.config.themes) do
209 if name:sub(1,1) ~= "." and pcall(tpl.Template,
210 "themes/%s/header" % fs.basename(theme)) then
214 assert(media, "No valid theme found")
217 local viewns = setmetatable({}, {__index=function(table, key)
218 if key == "controller" then
220 elseif key == "REQUEST_URI" then
221 return build_url(unpack(ctx.requestpath))
223 return rawget(table, key) or _G[key]
226 tpl.context.viewns = viewns
227 viewns.write = luci.http.write
228 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
229 viewns.translate = function(...) return require("luci.i18n").translate(...) end
230 viewns.striptags = util.striptags
232 viewns.theme = fs.basename(media)
233 viewns.resource = luci.config.main.resourcebase
236 track.dependent = (track.dependent ~= false)
237 assert(not track.dependent or not track.auto, "Access Violation")
239 if track.sysauth then
240 local sauth = require "luci.sauth"
242 local authen = type(track.sysauth_authenticator) == "function"
243 and track.sysauth_authenticator
244 or authenticator[track.sysauth_authenticator]
246 local def = (type(track.sysauth) == "string") and track.sysauth
247 local accs = def and {track.sysauth} or track.sysauth
248 local sess = ctx.authsession
249 local verifytoken = false
251 sess = luci.http.getcookie("sysauth")
252 sess = sess and sess:match("^[A-F0-9]+$")
256 local sdat = sauth.read(sess)
260 sdat = loadstring(sdat)()
261 if not verifytoken or ctx.urltoken.stok == sdat.token then
266 if not util.contains(accs, user) then
268 ctx.urltoken.stok = nil
269 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
270 if not user or not util.contains(accs, user) then
273 local sid = sess or luci.sys.uniqueid(16)
275 local token = luci.sys.uniqueid(16)
276 sauth.write(sid, util.get_bytecode({
279 secret=luci.sys.uniqueid(16)
281 ctx.urltoken.stok = token
283 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
284 ctx.authsession = sid
287 luci.http.status(403, "Forbidden")
291 ctx.authsession = sess
295 if track.setgroup then
296 luci.sys.process.setgroup(track.setgroup)
299 if track.setuser then
300 luci.sys.process.setuser(track.setuser)
305 if type(c.target) == "function" then
307 elseif type(c.target) == "table" then
308 target = c.target.target
312 if c and (c.index or type(target) == "function") then
314 ctx.requested = ctx.requested or ctx.dispatched
317 if c and c.index then
318 local tpl = require "luci.template"
320 if util.copcall(tpl.render, "indexer", {}) then
325 if type(target) == "function" then
326 util.copcall(function()
327 local oldenv = getfenv(target)
328 local module = require(c.module)
329 local env = setmetatable({}, {__index=
332 return rawget(tbl, key) or module[key] or oldenv[key]
338 if type(c.target) == "table" then
339 target(c.target, unpack(args))
348 --- Generate the dispatching index using the best possible strategy.
349 function createindex()
350 local path = luci.util.libpath() .. "/controller/"
353 if luci.util.copcall(require, "luci.fastindex") then
354 createindex_fastindex(path, suff)
356 createindex_plain(path, suff)
360 --- Generate the dispatching index using the fastindex C-indexer.
361 -- @param path Controller base directory
362 -- @param suffix Controller file suffix
363 function createindex_fastindex(path, suffix)
367 fi = luci.fastindex.new("index")
368 fi.add(path .. "*" .. suffix)
369 fi.add(path .. "*/*" .. suffix)
373 for k, v in pairs(fi.indexes) do
378 --- Generate the dispatching index using the native file-cache based strategy.
379 -- @param path Controller base directory
380 -- @param suffix Controller file suffix
381 function createindex_plain(path, suffix)
382 local controllers = util.combine(
383 luci.fs.glob(path .. "*" .. suffix) or {},
384 luci.fs.glob(path .. "*/*" .. suffix) or {}
388 local cachedate = fs.mtime(indexcache)
391 for _, obj in ipairs(controllers) do
392 local omtime = fs.mtime(path .. "/" .. obj)
393 realdate = (omtime and omtime > realdate) and omtime or realdate
396 if cachedate > realdate then
398 sys.process.info("uid") == fs.stat(indexcache, "uid")
399 and fs.stat(indexcache, "mode") == "rw-------",
400 "Fatal: Indexcache is not sane!"
403 index = loadfile(indexcache)()
411 for i,c in ipairs(controllers) do
412 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
413 local mod = require(module)
414 local idx = mod.index
416 if type(idx) == "function" then
422 fs.writefile(indexcache, util.get_bytecode(index))
423 fs.chmod(indexcache, "a-rwx,u+rw")
427 --- Create the dispatching tree from the index.
428 -- Build the index before if it does not exist yet.
429 function createtree()
435 local tree = {nodes={}}
438 ctx.treecache = setmetatable({}, {__mode="v"})
442 -- Load default translation
443 require "luci.i18n".loadc("default")
445 local scope = setmetatable({}, {__index = luci.dispatcher})
447 for k, v in pairs(index) do
453 local function modisort(a,b)
454 return modi[a].order < modi[b].order
457 for _, v in util.spairs(modi, modisort) do
458 scope._NAME = v.module
459 setfenv(v.func, scope)
466 --- Register a tree modifier.
467 -- @param func Modifier function
468 -- @param order Modifier order value (optional)
469 function modifier(func, order)
470 context.modifiers[#context.modifiers+1] = {
478 --- Clone a node of the dispatching tree to another position.
479 -- @param path Virtual path destination
480 -- @param clone Virtual path source
481 -- @param title Destination node title (optional)
482 -- @param order Destination node order value (optional)
483 -- @return Dispatching tree node
484 function assign(path, clone, title, order)
485 local obj = node(unpack(path))
492 setmetatable(obj, {__index = _create_node(clone)})
497 --- Create a new dispatching node and define common parameters.
498 -- @param path Virtual path
499 -- @param target Target function to call when dispatched.
500 -- @param title Destination node title
501 -- @param order Destination node order value (optional)
502 -- @return Dispatching tree node
503 function entry(path, target, title, order)
504 local c = node(unpack(path))
509 c.module = getfenv(2)._NAME
514 --- Fetch or create a new dispatching node.
515 -- @param ... Virtual path
516 -- @return Dispatching tree node
518 local c = _create_node({...})
520 c.module = getfenv(2)._NAME
526 function _create_node(path, cache)
531 cache = cache or context.treecache
532 local name = table.concat(path, ".")
533 local c = cache[name]
536 local new = {nodes={}, auto=true, path=util.clone(path)}
537 local last = table.remove(path)
539 c = _create_node(path, cache)
552 --- Create a redirect to another dispatching node.
553 -- @param ... Virtual path destination
557 for _, r in ipairs({...}) do
565 --- Rewrite the first x path values of the request.
566 -- @param n Number of path values to replace
567 -- @param ... Virtual path to replace removed path values with
568 function rewrite(n, ...)
571 local dispatched = util.clone(context.dispatched)
574 table.remove(dispatched, 1)
577 for i, r in ipairs(req) do
578 table.insert(dispatched, i, r)
581 for _, r in ipairs({...}) do
582 dispatched[#dispatched+1] = r
590 local function _call(self, ...)
591 if #self.argv > 0 then
592 return getfenv()[self.name](unpack(self.argv), ...)
594 return getfenv()[self.name](...)
598 --- Create a function-call dispatching target.
599 -- @param name Target function of local controller
600 -- @param ... Additional parameters passed to the function
601 function call(name, ...)
602 return {type = "call", argv = {...}, name = name, target = _call}
606 local _template = function(self, ...)
607 require "luci.template".render(self.view)
610 --- Create a template render dispatching target.
611 -- @param name Template to be rendered
612 function template(name)
613 return {type = "template", view = name, target = _template}
617 local function _cbi(self, ...)
618 local cbi = require "luci.cbi"
619 local tpl = require "luci.template"
620 local http = require "luci.http"
622 local config = self.config or {}
623 local maps = cbi.load(self.model, ...)
627 for i, res in ipairs(maps) do
628 if config.autoapply then
629 res.autoapply = config.autoapply
631 local cstate = res:parse()
632 if not state or cstate < state then
637 if config.on_valid_to and state and state > 0 and state < 2 then
638 http.redirect(config.on_valid_to)
642 if config.on_changed_to and state and state > 1 then
643 http.redirect(config.on_changed_to)
647 if config.on_success_to and state and state > 0 then
648 http.redirect(config.on_success_to)
652 if config.state_handler then
653 if not config.state_handler(state, maps) then
658 local pageaction = true
659 http.header("X-CBI-State", state or 0)
660 tpl.render("cbi/header", {state = state})
661 for i, res in ipairs(maps) do
663 if res.pageaction == false then
667 tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
670 --- Create a CBI model dispatching target.
671 -- @param model CBI model to be rendered
672 function cbi(model, config)
673 return {type = "cbi", config = config, model = model, target = _cbi}
677 local function _arcombine(self, ...)
679 local target = #argv > 0 and self.targets[2] or self.targets[1]
680 setfenv(target.target, self.env)
681 target:target(unpack(argv))
684 --- Create a combined dispatching target for non argv and argv requests.
685 -- @param trg1 Overview Target
686 -- @param trg2 Detail Target
687 function arcombine(trg1, trg2)
688 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
692 local function _form(self, ...)
693 local cbi = require "luci.cbi"
694 local tpl = require "luci.template"
695 local http = require "luci.http"
697 local maps = luci.cbi.load(self.model, ...)
700 for i, res in ipairs(maps) do
701 local cstate = res:parse()
702 if not state or cstate < state then
707 http.header("X-CBI-State", state or 0)
709 for i, res in ipairs(maps) do
715 --- Create a CBI form model dispatching target.
716 -- @param model CBI form model tpo be rendered
718 return {type = "cbi", model = model, target = _form}