Sync our coxpcall() implementation to the newest upstream version in order to get access to the inner backtrace information and propagate these traces to the browser in luci.dispatcher.dispatch(). This should make tracking down runtime errors much easier. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
999 lines
22 KiB
Lua
999 lines
22 KiB
Lua
-- Copyright 2008 Steven Barth <steven@midlink.org>
|
|
-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
|
|
-- Licensed to the public under the Apache License 2.0.
|
|
|
|
local fs = require "nixio.fs"
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local http = require "luci.http"
|
|
local nixio = require "nixio", require "nixio.util"
|
|
|
|
module("luci.dispatcher", package.seeall)
|
|
context = util.threadlocal()
|
|
uci = require "luci.model.uci"
|
|
i18n = require "luci.i18n"
|
|
_M.fs = fs
|
|
|
|
-- Index table
|
|
local index = nil
|
|
|
|
-- Fastindex
|
|
local fi
|
|
|
|
|
|
function build_url(...)
|
|
local path = {...}
|
|
local url = { http.getenv("SCRIPT_NAME") or "" }
|
|
|
|
local p
|
|
for _, p in ipairs(path) do
|
|
if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
|
|
url[#url+1] = "/"
|
|
url[#url+1] = p
|
|
end
|
|
end
|
|
|
|
if #path == 0 then
|
|
url[#url+1] = "/"
|
|
end
|
|
|
|
return table.concat(url, "")
|
|
end
|
|
|
|
function node_visible(node)
|
|
if node then
|
|
return not (
|
|
(not node.title or #node.title == 0) or
|
|
(not node.target or node.hidden == true) or
|
|
(type(node.target) == "table" and node.target.type == "firstchild" and
|
|
(type(node.nodes) ~= "table" or not next(node.nodes)))
|
|
)
|
|
end
|
|
return false
|
|
end
|
|
|
|
function node_childs(node)
|
|
local rv = { }
|
|
if node then
|
|
local k, v
|
|
for k, v in util.spairs(node.nodes,
|
|
function(a, b)
|
|
return (node.nodes[a].order or 100)
|
|
< (node.nodes[b].order or 100)
|
|
end)
|
|
do
|
|
if node_visible(v) then
|
|
rv[#rv+1] = k
|
|
end
|
|
end
|
|
end
|
|
return rv
|
|
end
|
|
|
|
|
|
function error404(message)
|
|
http.status(404, "Not Found")
|
|
message = message or "Not Found"
|
|
|
|
local function render()
|
|
local template = require "luci.template"
|
|
template.render("error404")
|
|
end
|
|
|
|
if not util.copcall(render) then
|
|
http.prepare_content("text/plain")
|
|
http.write(message)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function error500(message)
|
|
util.perror(message)
|
|
if not context.template_header_sent then
|
|
http.status(500, "Internal Server Error")
|
|
http.prepare_content("text/plain")
|
|
http.write(message)
|
|
else
|
|
require("luci.template")
|
|
if not util.copcall(luci.template.render, "error500", {message=message}) then
|
|
http.prepare_content("text/plain")
|
|
http.write(message)
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function httpdispatch(request, prefix)
|
|
http.context.request = request
|
|
|
|
local r = {}
|
|
context.request = r
|
|
|
|
local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
|
|
|
|
if prefix then
|
|
for _, node in ipairs(prefix) do
|
|
r[#r+1] = node
|
|
end
|
|
end
|
|
|
|
local node
|
|
for node in pathinfo:gmatch("[^/%z]+") do
|
|
r[#r+1] = node
|
|
end
|
|
|
|
local stat, err = util.coxpcall(function()
|
|
dispatch(context.request)
|
|
end, error500)
|
|
|
|
http.close()
|
|
|
|
--context._disable_memtrace()
|
|
end
|
|
|
|
local function require_post_security(target)
|
|
if type(target) == "table" then
|
|
if type(target.post) == "table" then
|
|
local param_name, required_val, request_val
|
|
|
|
for param_name, required_val in pairs(target.post) do
|
|
request_val = http.formvalue(param_name)
|
|
|
|
if (type(required_val) == "string" and
|
|
request_val ~= required_val) or
|
|
(required_val == true and request_val == nil)
|
|
then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
return (target.post == true)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function test_post_security()
|
|
if http.getenv("REQUEST_METHOD") ~= "POST" then
|
|
http.status(405, "Method Not Allowed")
|
|
http.header("Allow", "POST")
|
|
return false
|
|
end
|
|
|
|
if http.formvalue("token") ~= context.authtoken then
|
|
http.status(403, "Forbidden")
|
|
luci.template.render("csrftoken")
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function session_retrieve(sid, allowed_users)
|
|
local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
|
|
|
|
if type(sdat) == "table" and
|
|
type(sdat.values) == "table" and
|
|
type(sdat.values.token) == "string" and
|
|
(not allowed_users or
|
|
util.contains(allowed_users, sdat.values.username))
|
|
then
|
|
uci:set_session_id(sid)
|
|
return sid, sdat.values
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
local function session_setup(user, pass, allowed_users)
|
|
if util.contains(allowed_users, user) then
|
|
local login = util.ubus("session", "login", {
|
|
username = user,
|
|
password = pass,
|
|
timeout = tonumber(luci.config.sauth.sessiontime)
|
|
})
|
|
|
|
local rp = context.requestpath
|
|
and table.concat(context.requestpath, "/") or ""
|
|
|
|
if type(login) == "table" and
|
|
type(login.ubus_rpc_session) == "string"
|
|
then
|
|
util.ubus("session", "set", {
|
|
ubus_rpc_session = login.ubus_rpc_session,
|
|
values = { token = sys.uniqueid(16) }
|
|
})
|
|
|
|
io.stderr:write("luci: accepted login on /%s for %s from %s\n"
|
|
%{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
|
|
|
|
return session_retrieve(login.ubus_rpc_session)
|
|
end
|
|
|
|
io.stderr:write("luci: failed login on /%s for %s from %s\n"
|
|
%{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
function dispatch(request)
|
|
--context._disable_memtrace = require "luci.debug".trap_memtrace("l")
|
|
local ctx = context
|
|
ctx.path = request
|
|
|
|
local conf = require "luci.config"
|
|
assert(conf.main,
|
|
"/etc/config/luci seems to be corrupt, unable to find section 'main'")
|
|
|
|
local i18n = require "luci.i18n"
|
|
local lang = conf.main.lang or "auto"
|
|
if lang == "auto" then
|
|
local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
|
|
for aclang in aclang:gmatch("[%w_-]+") do
|
|
local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
|
|
if country and culture then
|
|
local cc = "%s_%s" %{ country, culture:lower() }
|
|
if conf.languages[cc] then
|
|
lang = cc
|
|
break
|
|
elseif conf.languages[country] then
|
|
lang = country
|
|
break
|
|
end
|
|
elseif conf.languages[aclang] then
|
|
lang = aclang
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if lang == "auto" then
|
|
lang = i18n.default
|
|
end
|
|
i18n.setlanguage(lang)
|
|
|
|
local c = ctx.tree
|
|
local stat
|
|
if not c then
|
|
c = createtree()
|
|
end
|
|
|
|
local track = {}
|
|
local args = {}
|
|
ctx.args = args
|
|
ctx.requestargs = ctx.requestargs or args
|
|
local n
|
|
local preq = {}
|
|
local freq = {}
|
|
|
|
for i, s in ipairs(request) do
|
|
preq[#preq+1] = s
|
|
freq[#freq+1] = s
|
|
c = c.nodes[s]
|
|
n = i
|
|
if not c then
|
|
break
|
|
end
|
|
|
|
util.update(track, c)
|
|
|
|
if c.leaf then
|
|
break
|
|
end
|
|
end
|
|
|
|
if c and c.leaf then
|
|
for j=n+1, #request do
|
|
args[#args+1] = request[j]
|
|
freq[#freq+1] = request[j]
|
|
end
|
|
end
|
|
|
|
ctx.requestpath = ctx.requestpath or freq
|
|
ctx.path = preq
|
|
|
|
if track.i18n then
|
|
i18n.loadc(track.i18n)
|
|
end
|
|
|
|
-- Init template engine
|
|
if (c and c.index) or not track.notemplate then
|
|
local tpl = require("luci.template")
|
|
local media = track.mediaurlbase or luci.config.main.mediaurlbase
|
|
if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
|
|
media = nil
|
|
for name, theme in pairs(luci.config.themes) do
|
|
if name:sub(1,1) ~= "." and pcall(tpl.Template,
|
|
"themes/%s/header" % fs.basename(theme)) then
|
|
media = theme
|
|
end
|
|
end
|
|
assert(media, "No valid theme found")
|
|
end
|
|
|
|
local function _ifattr(cond, key, val)
|
|
if cond then
|
|
local env = getfenv(3)
|
|
local scope = (type(env.self) == "table") and env.self
|
|
if type(val) == "table" then
|
|
if not next(val) then
|
|
return ''
|
|
else
|
|
val = util.serialize_json(val)
|
|
end
|
|
end
|
|
return string.format(
|
|
' %s="%s"', tostring(key),
|
|
util.pcdata(tostring( val
|
|
or (type(env[key]) ~= "function" and env[key])
|
|
or (scope and type(scope[key]) ~= "function" and scope[key])
|
|
or "" ))
|
|
)
|
|
else
|
|
return ''
|
|
end
|
|
end
|
|
|
|
tpl.context.viewns = setmetatable({
|
|
write = http.write;
|
|
include = function(name) tpl.Template(name):render(getfenv(2)) end;
|
|
translate = i18n.translate;
|
|
translatef = i18n.translatef;
|
|
export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
|
|
striptags = util.striptags;
|
|
pcdata = util.pcdata;
|
|
media = media;
|
|
theme = fs.basename(media);
|
|
resource = luci.config.main.resourcebase;
|
|
ifattr = function(...) return _ifattr(...) end;
|
|
attr = function(...) return _ifattr(true, ...) end;
|
|
url = build_url;
|
|
}, {__index=function(tbl, key)
|
|
if key == "controller" then
|
|
return build_url()
|
|
elseif key == "REQUEST_URI" then
|
|
return build_url(unpack(ctx.requestpath))
|
|
elseif key == "FULL_REQUEST_URI" then
|
|
local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
|
|
local query = http.getenv("QUERY_STRING")
|
|
if query and #query > 0 then
|
|
url[#url+1] = "?"
|
|
url[#url+1] = query
|
|
end
|
|
return table.concat(url, "")
|
|
elseif key == "token" then
|
|
return ctx.authtoken
|
|
else
|
|
return rawget(tbl, key) or _G[key]
|
|
end
|
|
end})
|
|
end
|
|
|
|
track.dependent = (track.dependent ~= false)
|
|
assert(not track.dependent or not track.auto,
|
|
"Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
|
|
"has no parent node so the access to this location has been denied.\n" ..
|
|
"This is a software bug, please report this message at " ..
|
|
"https://github.com/openwrt/luci/issues"
|
|
)
|
|
|
|
if track.sysauth and not ctx.authsession then
|
|
local authen = track.sysauth_authenticator
|
|
local _, sid, sdat, default_user, allowed_users
|
|
|
|
if type(authen) == "string" and authen ~= "htmlauth" then
|
|
error500("Unsupported authenticator %q configured" % authen)
|
|
return
|
|
end
|
|
|
|
if type(track.sysauth) == "table" then
|
|
default_user, allowed_users = nil, track.sysauth
|
|
else
|
|
default_user, allowed_users = track.sysauth, { track.sysauth }
|
|
end
|
|
|
|
if type(authen) == "function" then
|
|
_, sid = authen(sys.user.checkpasswd, allowed_users)
|
|
else
|
|
sid = http.getcookie("sysauth")
|
|
end
|
|
|
|
sid, sdat = session_retrieve(sid, allowed_users)
|
|
|
|
if not (sid and sdat) and authen == "htmlauth" then
|
|
local user = http.getenv("HTTP_AUTH_USER")
|
|
local pass = http.getenv("HTTP_AUTH_PASS")
|
|
|
|
if user == nil and pass == nil then
|
|
user = http.formvalue("luci_username")
|
|
pass = http.formvalue("luci_password")
|
|
end
|
|
|
|
sid, sdat = session_setup(user, pass, allowed_users)
|
|
|
|
if not sid then
|
|
local tmpl = require "luci.template"
|
|
|
|
context.path = {}
|
|
|
|
http.status(403, "Forbidden")
|
|
tmpl.render(track.sysauth_template or "sysauth", {
|
|
duser = default_user,
|
|
fuser = user
|
|
})
|
|
|
|
return
|
|
end
|
|
|
|
http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
|
|
sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
|
|
})
|
|
http.redirect(build_url(unpack(ctx.requestpath)))
|
|
end
|
|
|
|
if not sid or not sdat then
|
|
http.status(403, "Forbidden")
|
|
return
|
|
end
|
|
|
|
ctx.authsession = sid
|
|
ctx.authtoken = sdat.token
|
|
ctx.authuser = sdat.username
|
|
end
|
|
|
|
if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
|
|
luci.http.status(200, "OK")
|
|
luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
|
|
luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
return
|
|
end
|
|
|
|
if c and require_post_security(c.target) then
|
|
if not test_post_security(c) then
|
|
return
|
|
end
|
|
end
|
|
|
|
if track.setgroup then
|
|
sys.process.setgroup(track.setgroup)
|
|
end
|
|
|
|
if track.setuser then
|
|
sys.process.setuser(track.setuser)
|
|
end
|
|
|
|
local target = nil
|
|
if c then
|
|
if type(c.target) == "function" then
|
|
target = c.target
|
|
elseif type(c.target) == "table" then
|
|
target = c.target.target
|
|
end
|
|
end
|
|
|
|
if c and (c.index or type(target) == "function") then
|
|
ctx.dispatched = c
|
|
ctx.requested = ctx.requested or ctx.dispatched
|
|
end
|
|
|
|
if c and c.index then
|
|
local tpl = require "luci.template"
|
|
|
|
if util.copcall(tpl.render, "indexer", {}) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
if type(target) == "function" then
|
|
util.copcall(function()
|
|
local oldenv = getfenv(target)
|
|
local module = require(c.module)
|
|
local env = setmetatable({}, {__index=
|
|
|
|
function(tbl, key)
|
|
return rawget(tbl, key) or module[key] or oldenv[key]
|
|
end})
|
|
|
|
setfenv(target, env)
|
|
end)
|
|
|
|
local ok, err
|
|
if type(c.target) == "table" then
|
|
ok, err = util.copcall(target, c.target, unpack(args))
|
|
else
|
|
ok, err = util.copcall(target, unpack(args))
|
|
end
|
|
if not ok then
|
|
error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
|
|
" dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
|
|
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
|
|
end
|
|
else
|
|
local root = node()
|
|
if not root or not root.target then
|
|
error404("No root node was registered, this usually happens if no module was installed.\n" ..
|
|
"Install luci-mod-admin-full and retry. " ..
|
|
"If the module is already installed, try removing the /tmp/luci-indexcache file.")
|
|
else
|
|
error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
|
|
"If this url belongs to an extension, make sure it is properly installed.\n" ..
|
|
"If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
|
|
end
|
|
end
|
|
end
|
|
|
|
function createindex()
|
|
local controllers = { }
|
|
local base = "%s/controller/" % util.libpath()
|
|
local _, path
|
|
|
|
for path in (fs.glob("%s*.lua" % base) or function() end) do
|
|
controllers[#controllers+1] = path
|
|
end
|
|
|
|
for path in (fs.glob("%s*/*.lua" % base) or function() end) do
|
|
controllers[#controllers+1] = path
|
|
end
|
|
|
|
if indexcache then
|
|
local cachedate = fs.stat(indexcache, "mtime")
|
|
if cachedate then
|
|
local realdate = 0
|
|
for _, obj in ipairs(controllers) do
|
|
local omtime = fs.stat(obj, "mtime")
|
|
realdate = (omtime and omtime > realdate) and omtime or realdate
|
|
end
|
|
|
|
if cachedate > realdate and sys.process.info("uid") == 0 then
|
|
assert(
|
|
sys.process.info("uid") == fs.stat(indexcache, "uid")
|
|
and fs.stat(indexcache, "modestr") == "rw-------",
|
|
"Fatal: Indexcache is not sane!"
|
|
)
|
|
|
|
index = loadfile(indexcache)()
|
|
return index
|
|
end
|
|
end
|
|
end
|
|
|
|
index = {}
|
|
|
|
for _, path in ipairs(controllers) do
|
|
local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
|
|
local mod = require(modname)
|
|
assert(mod ~= true,
|
|
"Invalid controller file found\n" ..
|
|
"The file '" .. path .. "' contains an invalid module line.\n" ..
|
|
"Please verify whether the module name is set to '" .. modname ..
|
|
"' - It must correspond to the file path!")
|
|
|
|
local idx = mod.index
|
|
assert(type(idx) == "function",
|
|
"Invalid controller file found\n" ..
|
|
"The file '" .. path .. "' contains no index() function.\n" ..
|
|
"Please make sure that the controller contains a valid " ..
|
|
"index function and verify the spelling!")
|
|
|
|
index[modname] = idx
|
|
end
|
|
|
|
if indexcache then
|
|
local f = nixio.open(indexcache, "w", 600)
|
|
f:writeall(util.get_bytecode(index))
|
|
f:close()
|
|
end
|
|
end
|
|
|
|
-- Build the index before if it does not exist yet.
|
|
function createtree()
|
|
if not index then
|
|
createindex()
|
|
end
|
|
|
|
local ctx = context
|
|
local tree = {nodes={}, inreq=true}
|
|
local modi = {}
|
|
|
|
ctx.treecache = setmetatable({}, {__mode="v"})
|
|
ctx.tree = tree
|
|
ctx.modifiers = modi
|
|
|
|
-- Load default translation
|
|
require "luci.i18n".loadc("base")
|
|
|
|
local scope = setmetatable({}, {__index = luci.dispatcher})
|
|
|
|
for k, v in pairs(index) do
|
|
scope._NAME = k
|
|
setfenv(v, scope)
|
|
v()
|
|
end
|
|
|
|
local function modisort(a,b)
|
|
return modi[a].order < modi[b].order
|
|
end
|
|
|
|
for _, v in util.spairs(modi, modisort) do
|
|
scope._NAME = v.module
|
|
setfenv(v.func, scope)
|
|
v.func()
|
|
end
|
|
|
|
return tree
|
|
end
|
|
|
|
function modifier(func, order)
|
|
context.modifiers[#context.modifiers+1] = {
|
|
func = func,
|
|
order = order or 0,
|
|
module
|
|
= getfenv(2)._NAME
|
|
}
|
|
end
|
|
|
|
function assign(path, clone, title, order)
|
|
local obj = node(unpack(path))
|
|
obj.nodes = nil
|
|
obj.module = nil
|
|
|
|
obj.title = title
|
|
obj.order = order
|
|
|
|
setmetatable(obj, {__index = _create_node(clone)})
|
|
|
|
return obj
|
|
end
|
|
|
|
function entry(path, target, title, order)
|
|
local c = node(unpack(path))
|
|
|
|
c.target = target
|
|
c.title = title
|
|
c.order = order
|
|
c.module = getfenv(2)._NAME
|
|
|
|
return c
|
|
end
|
|
|
|
-- enabling the node.
|
|
function get(...)
|
|
return _create_node({...})
|
|
end
|
|
|
|
function node(...)
|
|
local c = _create_node({...})
|
|
|
|
c.module = getfenv(2)._NAME
|
|
c.auto = nil
|
|
|
|
return c
|
|
end
|
|
|
|
function lookup(...)
|
|
local i, path = nil, {}
|
|
for i = 1, select('#', ...) do
|
|
local name, arg = nil, tostring(select(i, ...))
|
|
for name in arg:gmatch("[^/]+") do
|
|
path[#path+1] = name
|
|
end
|
|
end
|
|
|
|
for i = #path, 1, -1 do
|
|
local node = context.treecache[table.concat(path, ".", 1, i)]
|
|
if node and (i == #path or node.leaf) then
|
|
return node, build_url(unpack(path))
|
|
end
|
|
end
|
|
end
|
|
|
|
function _create_node(path)
|
|
if #path == 0 then
|
|
return context.tree
|
|
end
|
|
|
|
local name = table.concat(path, ".")
|
|
local c = context.treecache[name]
|
|
|
|
if not c then
|
|
local last = table.remove(path)
|
|
local parent = _create_node(path)
|
|
|
|
c = {nodes={}, auto=true}
|
|
-- the node is "in request" if the request path matches
|
|
-- at least up to the length of the node path
|
|
if parent.inreq and context.path[#path+1] == last then
|
|
c.inreq = true
|
|
end
|
|
parent.nodes[last] = c
|
|
context.treecache[name] = c
|
|
end
|
|
return c
|
|
end
|
|
|
|
-- Subdispatchers --
|
|
|
|
function _firstchild()
|
|
local path = { unpack(context.path) }
|
|
local name = table.concat(path, ".")
|
|
local node = context.treecache[name]
|
|
|
|
local lowest
|
|
if node and node.nodes and next(node.nodes) then
|
|
local k, v
|
|
for k, v in pairs(node.nodes) do
|
|
if not lowest or
|
|
(v.order or 100) < (node.nodes[lowest].order or 100)
|
|
then
|
|
lowest = k
|
|
end
|
|
end
|
|
end
|
|
|
|
assert(lowest ~= nil,
|
|
"The requested node contains no childs, unable to redispatch")
|
|
|
|
path[#path+1] = lowest
|
|
dispatch(path)
|
|
end
|
|
|
|
function firstchild()
|
|
return { type = "firstchild", target = _firstchild }
|
|
end
|
|
|
|
function alias(...)
|
|
local req = {...}
|
|
return function(...)
|
|
for _, r in ipairs({...}) do
|
|
req[#req+1] = r
|
|
end
|
|
|
|
dispatch(req)
|
|
end
|
|
end
|
|
|
|
function rewrite(n, ...)
|
|
local req = {...}
|
|
return function(...)
|
|
local dispatched = util.clone(context.dispatched)
|
|
|
|
for i=1,n do
|
|
table.remove(dispatched, 1)
|
|
end
|
|
|
|
for i, r in ipairs(req) do
|
|
table.insert(dispatched, i, r)
|
|
end
|
|
|
|
for _, r in ipairs({...}) do
|
|
dispatched[#dispatched+1] = r
|
|
end
|
|
|
|
dispatch(dispatched)
|
|
end
|
|
end
|
|
|
|
|
|
local function _call(self, ...)
|
|
local func = getfenv()[self.name]
|
|
assert(func ~= nil,
|
|
'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
|
|
|
|
assert(type(func) == "function",
|
|
'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
|
|
'of type "' .. type(func) .. '".')
|
|
|
|
if #self.argv > 0 then
|
|
return func(unpack(self.argv), ...)
|
|
else
|
|
return func(...)
|
|
end
|
|
end
|
|
|
|
function call(name, ...)
|
|
return {type = "call", argv = {...}, name = name, target = _call}
|
|
end
|
|
|
|
function post_on(params, name, ...)
|
|
return {
|
|
type = "call",
|
|
post = params,
|
|
argv = { ... },
|
|
name = name,
|
|
target = _call
|
|
}
|
|
end
|
|
|
|
function post(...)
|
|
return post_on(true, ...)
|
|
end
|
|
|
|
|
|
local _template = function(self, ...)
|
|
require "luci.template".render(self.view)
|
|
end
|
|
|
|
function template(name)
|
|
return {type = "template", view = name, target = _template}
|
|
end
|
|
|
|
|
|
local function _cbi(self, ...)
|
|
local cbi = require "luci.cbi"
|
|
local tpl = require "luci.template"
|
|
local http = require "luci.http"
|
|
|
|
local config = self.config or {}
|
|
local maps = cbi.load(self.model, ...)
|
|
|
|
local state = nil
|
|
|
|
local i, res
|
|
for i, res in ipairs(maps) do
|
|
if util.instanceof(res, cbi.SimpleForm) then
|
|
io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
|
|
% self.model)
|
|
|
|
io.stderr:write("please change %s to use the form() action instead.\n"
|
|
% table.concat(context.request, "/"))
|
|
end
|
|
|
|
res.flow = config
|
|
local cstate = res:parse()
|
|
if cstate and (not state or cstate < state) then
|
|
state = cstate
|
|
end
|
|
end
|
|
|
|
local function _resolve_path(path)
|
|
return type(path) == "table" and build_url(unpack(path)) or path
|
|
end
|
|
|
|
if config.on_valid_to and state and state > 0 and state < 2 then
|
|
http.redirect(_resolve_path(config.on_valid_to))
|
|
return
|
|
end
|
|
|
|
if config.on_changed_to and state and state > 1 then
|
|
http.redirect(_resolve_path(config.on_changed_to))
|
|
return
|
|
end
|
|
|
|
if config.on_success_to and state and state > 0 then
|
|
http.redirect(_resolve_path(config.on_success_to))
|
|
return
|
|
end
|
|
|
|
if config.state_handler then
|
|
if not config.state_handler(state, maps) then
|
|
return
|
|
end
|
|
end
|
|
|
|
http.header("X-CBI-State", state or 0)
|
|
|
|
if not config.noheader then
|
|
tpl.render("cbi/header", {state = state})
|
|
end
|
|
|
|
local redirect
|
|
local messages
|
|
local applymap = false
|
|
local pageaction = true
|
|
local parsechain = { }
|
|
|
|
local is_rollback, time_remaining = uci:rollback_pending()
|
|
|
|
for i, res in ipairs(maps) do
|
|
if res.apply_needed and res.parsechain then
|
|
local c
|
|
for _, c in ipairs(res.parsechain) do
|
|
parsechain[#parsechain+1] = c
|
|
end
|
|
applymap = true
|
|
end
|
|
|
|
if res.redirect then
|
|
redirect = redirect or res.redirect
|
|
end
|
|
|
|
if res.pageaction == false then
|
|
pageaction = false
|
|
end
|
|
|
|
if res.message then
|
|
messages = messages or { }
|
|
messages[#messages+1] = res.message
|
|
end
|
|
end
|
|
|
|
for i, res in ipairs(maps) do
|
|
res:render({
|
|
firstmap = (i == 1),
|
|
applymap = applymap,
|
|
confirmmap = (is_rollback and time_remaining or nil),
|
|
redirect = redirect,
|
|
messages = messages,
|
|
pageaction = pageaction,
|
|
parsechain = parsechain
|
|
})
|
|
end
|
|
|
|
if not config.nofooter then
|
|
tpl.render("cbi/footer", {
|
|
flow = config,
|
|
pageaction = pageaction,
|
|
redirect = redirect,
|
|
state = state,
|
|
autoapply = config.autoapply
|
|
})
|
|
end
|
|
end
|
|
|
|
function cbi(model, config)
|
|
return {
|
|
type = "cbi",
|
|
post = { ["cbi.submit"] = true },
|
|
config = config,
|
|
model = model,
|
|
target = _cbi
|
|
}
|
|
end
|
|
|
|
|
|
local function _arcombine(self, ...)
|
|
local argv = {...}
|
|
local target = #argv > 0 and self.targets[2] or self.targets[1]
|
|
setfenv(target.target, self.env)
|
|
target:target(unpack(argv))
|
|
end
|
|
|
|
function arcombine(trg1, trg2)
|
|
return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
|
|
end
|
|
|
|
|
|
local function _form(self, ...)
|
|
local cbi = require "luci.cbi"
|
|
local tpl = require "luci.template"
|
|
local http = require "luci.http"
|
|
|
|
local maps = luci.cbi.load(self.model, ...)
|
|
local state = nil
|
|
|
|
local i, res
|
|
for i, res in ipairs(maps) do
|
|
local cstate = res:parse()
|
|
if cstate and (not state or cstate < state) then
|
|
state = cstate
|
|
end
|
|
end
|
|
|
|
http.header("X-CBI-State", state or 0)
|
|
tpl.render("header")
|
|
for i, res in ipairs(maps) do
|
|
res:render()
|
|
end
|
|
tpl.render("footer")
|
|
end
|
|
|
|
function form(model)
|
|
return {
|
|
type = "cbi",
|
|
post = { ["cbi.submit"] = true },
|
|
model = model,
|
|
target = _form
|
|
}
|
|
end
|
|
|
|
translate = i18n.translate
|
|
|
|
-- This function does not actually translate the given argument but
|
|
-- is used by build/i18n-scan.pl to find translatable entries.
|
|
function _(text)
|
|
return text
|
|
end
|