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 lpat = lpat and lpat:gsub("-", "_")
139 if conf.languages[lpat] then
145 require "luci.i18n".setlanguage(lang)
156 ctx.requestargs = ctx.requestargs or args
159 local token = ctx.urltoken
163 for i, s in ipairs(request) do
166 tkey, tval = s:match(";(%w+)=(.*)")
181 util.update(track, c)
190 for j=n+1, #request do
191 args[#args+1] = request[j]
192 freq[#freq+1] = request[j]
196 ctx.requestpath = freq
200 require("luci.i18n").loadc(track.i18n)
203 -- Init template engine
204 if (c and c.index) or not track.notemplate then
205 local tpl = require("luci.template")
206 local media = track.mediaurlbase or luci.config.main.mediaurlbase
207 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
209 for name, theme in pairs(luci.config.themes) do
210 if name:sub(1,1) ~= "." and pcall(tpl.Template,
211 "themes/%s/header" % fs.basename(theme)) then
215 assert(media, "No valid theme found")
218 local viewns = setmetatable({}, {__index=function(table, key)
219 if key == "controller" then
221 elseif key == "REQUEST_URI" then
222 return build_url(unpack(ctx.requestpath))
224 return rawget(table, key) or _G[key]
227 tpl.context.viewns = viewns
228 viewns.write = luci.http.write
229 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
230 viewns.translate = function(...) return require("luci.i18n").translate(...) end
231 viewns.striptags = util.striptags
233 viewns.theme = fs.basename(media)
234 viewns.resource = luci.config.main.resourcebase
237 track.dependent = (track.dependent ~= false)
238 assert(not track.dependent or not track.auto, "Access Violation")
240 if track.sysauth then
241 local sauth = require "luci.sauth"
243 local authen = type(track.sysauth_authenticator) == "function"
244 and track.sysauth_authenticator
245 or authenticator[track.sysauth_authenticator]
247 local def = (type(track.sysauth) == "string") and track.sysauth
248 local accs = def and {track.sysauth} or track.sysauth
249 local sess = ctx.authsession
250 local verifytoken = false
252 sess = luci.http.getcookie("sysauth")
253 sess = sess and sess:match("^[A-F0-9]+$")
257 local sdat = sauth.read(sess)
261 sdat = loadstring(sdat)()
262 if not verifytoken or ctx.urltoken.stok == sdat.token then
267 if not util.contains(accs, user) then
269 ctx.urltoken.stok = nil
270 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
271 if not user or not util.contains(accs, user) then
274 local sid = sess or luci.sys.uniqueid(16)
276 local token = luci.sys.uniqueid(16)
277 sauth.write(sid, util.get_bytecode({
280 secret=luci.sys.uniqueid(16)
282 ctx.urltoken.stok = token
284 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
285 ctx.authsession = sid
288 luci.http.status(403, "Forbidden")
292 ctx.authsession = sess
296 if track.setgroup then
297 luci.sys.process.setgroup(track.setgroup)
300 if track.setuser then
301 luci.sys.process.setuser(track.setuser)
306 if type(c.target) == "function" then
308 elseif type(c.target) == "table" then
309 target = c.target.target
313 if c and (c.index or type(target) == "function") then
315 ctx.requested = ctx.requested or ctx.dispatched
318 if c and c.index then
319 local tpl = require "luci.template"
321 if util.copcall(tpl.render, "indexer", {}) then
326 if type(target) == "function" then
327 util.copcall(function()
328 local oldenv = getfenv(target)
329 local module = require(c.module)
330 local env = setmetatable({}, {__index=
333 return rawget(tbl, key) or module[key] or oldenv[key]
339 if type(c.target) == "table" then
340 target(c.target, unpack(args))
349 --- Generate the dispatching index using the best possible strategy.
350 function createindex()
351 local path = luci.util.libpath() .. "/controller/"
354 if luci.util.copcall(require, "luci.fastindex") then
355 createindex_fastindex(path, suff)
357 createindex_plain(path, suff)
361 --- Generate the dispatching index using the fastindex C-indexer.
362 -- @param path Controller base directory
363 -- @param suffix Controller file suffix
364 function createindex_fastindex(path, suffix)
368 fi = luci.fastindex.new("index")
369 fi.add(path .. "*" .. suffix)
370 fi.add(path .. "*/*" .. suffix)
374 for k, v in pairs(fi.indexes) do
379 --- Generate the dispatching index using the native file-cache based strategy.
380 -- @param path Controller base directory
381 -- @param suffix Controller file suffix
382 function createindex_plain(path, suffix)
383 local controllers = util.combine(
384 luci.fs.glob(path .. "*" .. suffix) or {},
385 luci.fs.glob(path .. "*/*" .. suffix) or {}
389 local cachedate = fs.mtime(indexcache)
392 for _, obj in ipairs(controllers) do
393 local omtime = fs.mtime(path .. "/" .. obj)
394 realdate = (omtime and omtime > realdate) and omtime or realdate
397 if cachedate > realdate then
399 sys.process.info("uid") == fs.stat(indexcache, "uid")
400 and fs.stat(indexcache, "mode") == "rw-------",
401 "Fatal: Indexcache is not sane!"
404 index = loadfile(indexcache)()
412 for i,c in ipairs(controllers) do
413 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
414 local mod = require(module)
415 local idx = mod.index
417 if type(idx) == "function" then
423 fs.writefile(indexcache, util.get_bytecode(index))
424 fs.chmod(indexcache, "a-rwx,u+rw")
428 --- Create the dispatching tree from the index.
429 -- Build the index before if it does not exist yet.
430 function createtree()
436 local tree = {nodes={}}
439 ctx.treecache = setmetatable({}, {__mode="v"})
443 -- Load default translation
444 require "luci.i18n".loadc("default")
446 local scope = setmetatable({}, {__index = luci.dispatcher})
448 for k, v in pairs(index) do
454 local function modisort(a,b)
455 return modi[a].order < modi[b].order
458 for _, v in util.spairs(modi, modisort) do
459 scope._NAME = v.module
460 setfenv(v.func, scope)
467 --- Register a tree modifier.
468 -- @param func Modifier function
469 -- @param order Modifier order value (optional)
470 function modifier(func, order)
471 context.modifiers[#context.modifiers+1] = {
479 --- Clone a node of the dispatching tree to another position.
480 -- @param path Virtual path destination
481 -- @param clone Virtual path source
482 -- @param title Destination node title (optional)
483 -- @param order Destination node order value (optional)
484 -- @return Dispatching tree node
485 function assign(path, clone, title, order)
486 local obj = node(unpack(path))
493 setmetatable(obj, {__index = _create_node(clone)})
498 --- Create a new dispatching node and define common parameters.
499 -- @param path Virtual path
500 -- @param target Target function to call when dispatched.
501 -- @param title Destination node title
502 -- @param order Destination node order value (optional)
503 -- @return Dispatching tree node
504 function entry(path, target, title, order)
505 local c = node(unpack(path))
510 c.module = getfenv(2)._NAME
515 --- Fetch or create a new dispatching node.
516 -- @param ... Virtual path
517 -- @return Dispatching tree node
519 local c = _create_node({...})
521 c.module = getfenv(2)._NAME
527 function _create_node(path, cache)
532 cache = cache or context.treecache
533 local name = table.concat(path, ".")
534 local c = cache[name]
537 local new = {nodes={}, auto=true, path=util.clone(path)}
538 local last = table.remove(path)
540 c = _create_node(path, cache)
553 --- Create a redirect to another dispatching node.
554 -- @param ... Virtual path destination
558 for _, r in ipairs({...}) do
566 --- Rewrite the first x path values of the request.
567 -- @param n Number of path values to replace
568 -- @param ... Virtual path to replace removed path values with
569 function rewrite(n, ...)
572 local dispatched = util.clone(context.dispatched)
575 table.remove(dispatched, 1)
578 for i, r in ipairs(req) do
579 table.insert(dispatched, i, r)
582 for _, r in ipairs({...}) do
583 dispatched[#dispatched+1] = r
591 local function _call(self, ...)
592 if #self.argv > 0 then
593 return getfenv()[self.name](unpack(self.argv), ...)
595 return getfenv()[self.name](...)
599 --- Create a function-call dispatching target.
600 -- @param name Target function of local controller
601 -- @param ... Additional parameters passed to the function
602 function call(name, ...)
603 return {type = "call", argv = {...}, name = name, target = _call}
607 local _template = function(self, ...)
608 require "luci.template".render(self.view)
611 --- Create a template render dispatching target.
612 -- @param name Template to be rendered
613 function template(name)
614 return {type = "template", view = name, target = _template}
618 local function _cbi(self, ...)
619 local cbi = require "luci.cbi"
620 local tpl = require "luci.template"
621 local http = require "luci.http"
623 local config = self.config or {}
624 local maps = cbi.load(self.model, ...)
628 for i, res in ipairs(maps) do
629 if config.autoapply then
630 res.autoapply = config.autoapply
632 local cstate = res:parse()
633 if not state or cstate < state then
638 if config.on_valid_to and state and state > 0 and state < 2 then
639 http.redirect(config.on_valid_to)
643 if config.on_changed_to and state and state > 1 then
644 http.redirect(config.on_changed_to)
648 if config.on_success_to and state and state > 0 then
649 http.redirect(config.on_success_to)
653 if config.state_handler then
654 if not config.state_handler(state, maps) then
659 local pageaction = true
660 http.header("X-CBI-State", state or 0)
661 tpl.render("cbi/header", {state = state})
662 for i, res in ipairs(maps) do
664 if res.pageaction == false then
668 tpl.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
671 --- Create a CBI model dispatching target.
672 -- @param model CBI model to be rendered
673 function cbi(model, config)
674 return {type = "cbi", config = config, model = model, target = _cbi}
678 local function _arcombine(self, ...)
680 local target = #argv > 0 and self.targets[2] or self.targets[1]
681 setfenv(target.target, self.env)
682 target:target(unpack(argv))
685 --- Create a combined dispatching target for non argv and argv requests.
686 -- @param trg1 Overview Target
687 -- @param trg2 Detail Target
688 function arcombine(trg1, trg2)
689 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
693 local function _form(self, ...)
694 local cbi = require "luci.cbi"
695 local tpl = require "luci.template"
696 local http = require "luci.http"
698 local maps = luci.cbi.load(self.model, ...)
701 for i, res in ipairs(maps) do
702 local cstate = res:parse()
703 if not state or cstate < state then
708 http.header("X-CBI-State", state or 0)
710 for i, res in ipairs(maps) do
716 --- Create a CBI form model dispatching target.
717 -- @param model CBI form model tpo be rendered
719 return {type = "cbi", model = model, target = _form}